sequel 5.30.0 → 5.35.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +86 -0
  3. data/README.rdoc +1 -1
  4. data/doc/advanced_associations.rdoc +4 -4
  5. data/doc/association_basics.rdoc +10 -5
  6. data/doc/code_order.rdoc +12 -2
  7. data/doc/dataset_filtering.rdoc +2 -2
  8. data/doc/model_dataset_method_design.rdoc +1 -1
  9. data/doc/postgresql.rdoc +71 -0
  10. data/doc/release_notes/5.31.0.txt +148 -0
  11. data/doc/release_notes/5.32.0.txt +46 -0
  12. data/doc/release_notes/5.33.0.txt +24 -0
  13. data/doc/release_notes/5.34.0.txt +40 -0
  14. data/doc/release_notes/5.35.0.txt +56 -0
  15. data/doc/testing.rdoc +1 -1
  16. data/lib/sequel/adapters/oracle.rb +2 -1
  17. data/lib/sequel/adapters/shared/access.rb +6 -6
  18. data/lib/sequel/adapters/shared/mssql.rb +5 -5
  19. data/lib/sequel/adapters/shared/mysql.rb +9 -9
  20. data/lib/sequel/adapters/shared/oracle.rb +16 -16
  21. data/lib/sequel/adapters/shared/postgres.rb +169 -14
  22. data/lib/sequel/adapters/shared/sqlanywhere.rb +9 -9
  23. data/lib/sequel/adapters/shared/sqlite.rb +33 -6
  24. data/lib/sequel/adapters/tinytds.rb +1 -0
  25. data/lib/sequel/connection_pool/sharded_single.rb +4 -1
  26. data/lib/sequel/connection_pool/sharded_threaded.rb +12 -12
  27. data/lib/sequel/connection_pool/single.rb +1 -1
  28. data/lib/sequel/connection_pool/threaded.rb +2 -2
  29. data/lib/sequel/core.rb +318 -314
  30. data/lib/sequel/database/connecting.rb +1 -1
  31. data/lib/sequel/database/misc.rb +16 -10
  32. data/lib/sequel/database/query.rb +3 -1
  33. data/lib/sequel/database/schema_generator.rb +0 -1
  34. data/lib/sequel/database/schema_methods.rb +15 -16
  35. data/lib/sequel/database/transactions.rb +7 -4
  36. data/lib/sequel/dataset/placeholder_literalizer.rb +3 -7
  37. data/lib/sequel/dataset/query.rb +5 -4
  38. data/lib/sequel/deprecated.rb +3 -1
  39. data/lib/sequel/exceptions.rb +2 -0
  40. data/lib/sequel/extensions/_pretty_table.rb +1 -2
  41. data/lib/sequel/extensions/columns_introspection.rb +1 -2
  42. data/lib/sequel/extensions/connection_expiration.rb +2 -2
  43. data/lib/sequel/extensions/connection_validator.rb +2 -2
  44. data/lib/sequel/extensions/core_refinements.rb +2 -0
  45. data/lib/sequel/extensions/duplicate_columns_handler.rb +2 -0
  46. data/lib/sequel/extensions/fiber_concurrency.rb +24 -0
  47. data/lib/sequel/extensions/index_caching.rb +9 -7
  48. data/lib/sequel/extensions/integer64.rb +2 -0
  49. data/lib/sequel/extensions/migration.rb +1 -2
  50. data/lib/sequel/extensions/pg_array_ops.rb +4 -0
  51. data/lib/sequel/extensions/pg_enum.rb +7 -2
  52. data/lib/sequel/extensions/pg_extended_date_support.rb +1 -1
  53. data/lib/sequel/extensions/pg_hstore.rb +6 -0
  54. data/lib/sequel/extensions/pg_hstore_ops.rb +2 -0
  55. data/lib/sequel/extensions/pg_inet.rb +15 -5
  56. data/lib/sequel/extensions/pg_interval.rb +2 -0
  57. data/lib/sequel/extensions/pg_json_ops.rb +2 -0
  58. data/lib/sequel/extensions/pg_range.rb +5 -7
  59. data/lib/sequel/extensions/pg_range_ops.rb +2 -0
  60. data/lib/sequel/extensions/pg_row.rb +0 -1
  61. data/lib/sequel/extensions/pg_timestamptz.rb +2 -0
  62. data/lib/sequel/extensions/run_transaction_hooks.rb +72 -0
  63. data/lib/sequel/extensions/s.rb +2 -0
  64. data/lib/sequel/extensions/schema_dumper.rb +10 -4
  65. data/lib/sequel/extensions/server_block.rb +3 -3
  66. data/lib/sequel/extensions/symbol_aref_refinement.rb +2 -0
  67. data/lib/sequel/extensions/symbol_as_refinement.rb +2 -0
  68. data/lib/sequel/extensions/to_dot.rb +9 -3
  69. data/lib/sequel/model.rb +2 -0
  70. data/lib/sequel/model/associations.rb +54 -25
  71. data/lib/sequel/model/base.rb +70 -57
  72. data/lib/sequel/model/plugins.rb +3 -3
  73. data/lib/sequel/plugins/association_lazy_eager_option.rb +66 -0
  74. data/lib/sequel/plugins/association_multi_add_remove.rb +2 -0
  75. data/lib/sequel/plugins/association_pks.rb +60 -18
  76. data/lib/sequel/plugins/association_proxies.rb +2 -0
  77. data/lib/sequel/plugins/blacklist_security.rb +1 -2
  78. data/lib/sequel/plugins/boolean_subsets.rb +4 -1
  79. data/lib/sequel/plugins/class_table_inheritance.rb +28 -28
  80. data/lib/sequel/plugins/csv_serializer.rb +2 -0
  81. data/lib/sequel/plugins/dirty.rb +13 -13
  82. data/lib/sequel/plugins/forbid_lazy_load.rb +216 -0
  83. data/lib/sequel/plugins/instance_specific_default.rb +113 -0
  84. data/lib/sequel/plugins/json_serializer.rb +3 -7
  85. data/lib/sequel/plugins/lazy_attributes.rb +1 -1
  86. data/lib/sequel/plugins/pg_array_associations.rb +2 -3
  87. data/lib/sequel/plugins/prepared_statements.rb +5 -11
  88. data/lib/sequel/plugins/prepared_statements_safe.rb +1 -3
  89. data/lib/sequel/plugins/rcte_tree.rb +10 -16
  90. data/lib/sequel/plugins/single_table_inheritance.rb +15 -15
  91. data/lib/sequel/plugins/skip_saving_columns.rb +108 -0
  92. data/lib/sequel/plugins/string_stripper.rb +1 -1
  93. data/lib/sequel/plugins/subclasses.rb +2 -0
  94. data/lib/sequel/plugins/validation_class_methods.rb +5 -1
  95. data/lib/sequel/timezones.rb +6 -4
  96. data/lib/sequel/version.rb +1 -1
  97. metadata +18 -2
@@ -312,7 +312,9 @@ module Sequel
312
312
  end
313
313
  end
314
314
 
315
+ # :nocov:
315
316
  if defined?(HStore)
317
+ # :nocov:
316
318
  class HStore
317
319
  # Wrap the receiver in an HStoreOp so you can easily use the PostgreSQL
318
320
  # hstore functions and operators with it.
@@ -40,12 +40,22 @@ module Sequel
40
40
  def self.extended(db)
41
41
  db.instance_exec do
42
42
  extend_datasets(InetDatasetMethods)
43
- meth = IPAddr.method(:new)
44
- add_conversion_proc(869, meth)
45
- add_conversion_proc(650, meth)
43
+
44
+ # :nocov:
45
+ if !defined?(SEQUEL_PG_VERSION_INTEGER) || SEQUEL_PG_VERSION_INTEGER >= 11300
46
+ # :nocov:
47
+ # sequel_pg 1.13.0+ will use inet/cidr conversion procs, but doing so is
48
+ # slower, so don't add the conversion procs if using sequel_pg 1.13.0+.
49
+ meth = IPAddr.method(:new)
50
+ add_conversion_proc(869, meth)
51
+ add_conversion_proc(650, meth)
52
+ if respond_to?(:register_array_type)
53
+ register_array_type('inet', :oid=>1041, :scalar_oid=>869)
54
+ register_array_type('cidr', :oid=>651, :scalar_oid=>650)
55
+ end
56
+ end
57
+
46
58
  if respond_to?(:register_array_type)
47
- register_array_type('inet', :oid=>1041, :scalar_oid=>869)
48
- register_array_type('cidr', :oid=>651, :scalar_oid=>650)
49
59
  register_array_type('macaddr', :oid=>1040, :scalar_oid=>829)
50
60
  end
51
61
  @schema_type_classes[:ipaddr] = IPAddr
@@ -178,6 +178,8 @@ module Sequel
178
178
  end
179
179
 
180
180
  module IntervalDatasetMethods
181
+ private
182
+
181
183
  # Handle literalization of ActiveSupport::Duration objects, treating them as
182
184
  # PostgreSQL intervals.
183
185
  def literal_other_append(sql, v)
@@ -554,7 +554,9 @@ module Sequel
554
554
  end
555
555
  end
556
556
 
557
+ # :nocov:
557
558
  if defined?(JSONArray)
559
+ # :nocov:
558
560
  class JSONArray
559
561
  # Wrap the JSONArray instance in an JSONOp, allowing you to easily use
560
562
  # the PostgreSQL json functions and operators with literal jsons.
@@ -158,7 +158,7 @@ module Sequel
158
158
  procs = conversion_procs
159
159
  add_conversion_proc(3908, Parser.new("tsrange", procs[1114]))
160
160
  add_conversion_proc(3910, Parser.new("tstzrange", procs[1184]))
161
- if defined?(PGArray::Creator)
161
+ if respond_to?(:register_array_type) && defined?(PGArray::Creator)
162
162
  add_conversion_proc(3909, PGArray::Creator.new("tsrange", procs[3908]))
163
163
  add_conversion_proc(3911, PGArray::Creator.new("tstzrange", procs[3910]))
164
164
  end
@@ -215,12 +215,6 @@ module Sequel
215
215
 
216
216
  db_type = db_type.to_s.dup.freeze
217
217
 
218
- if converter = opts[:converter]
219
- raise Error, "can't provide both a block and :converter option to register" if block
220
- else
221
- converter = block
222
- end
223
-
224
218
  if soid
225
219
  raise Error, "can't provide both a converter and :subtype_oid option to register" if has_converter
226
220
  raise Error, "no conversion proc for :subtype_oid=>#{soid.inspect} in conversion_procs" unless converter = conversion_procs[soid]
@@ -304,6 +298,8 @@ module Sequel
304
298
  end
305
299
 
306
300
  module DatasetMethods
301
+ private
302
+
307
303
  # Handle literalization of ruby Range objects, treating them as
308
304
  # PostgreSQL ranges.
309
305
  def literal_other_append(sql, v)
@@ -469,8 +465,10 @@ module Sequel
469
465
  return @range if @range
470
466
  raise(Error, "cannot create ruby range for an empty PostgreSQL range") if empty?
471
467
  raise(Error, "cannot create ruby range when PostgreSQL range excludes beginning element") if exclude_begin?
468
+ # :nocov:
472
469
  raise(Error, "cannot create ruby range when PostgreSQL range has unbounded beginning") if STARTLESS_RANGE_NOT_SUPPORTED && !self.begin
473
470
  raise(Error, "cannot create ruby range when PostgreSQL range has unbounded ending") if ENDLESS_RANGE_NOT_SUPPORTED && !self.end
471
+ # :nocov:
474
472
  @range = Range.new(self.begin, self.end, exclude_end?)
475
473
  end
476
474
 
@@ -116,7 +116,9 @@ module Sequel
116
116
  end
117
117
  end
118
118
 
119
+ # :nocov:
119
120
  if defined?(PGRange)
121
+ # :nocov:
120
122
  class PGRange
121
123
  # Wrap the PGRange instance in an RangeOp, allowing you to easily use
122
124
  # the PostgreSQL range functions and operators with literal ranges.
@@ -222,7 +222,6 @@ module Sequel
222
222
  # Split the stored string into an array of strings, handling
223
223
  # the different types of quoting.
224
224
  def parse
225
- return @result if @result
226
225
  values = []
227
226
  skip(/\(/)
228
227
  if skip(/\)/)
@@ -15,6 +15,8 @@
15
15
  module Sequel
16
16
  module Postgres
17
17
  module Timestamptz
18
+ private
19
+
18
20
  # Use timestamptz by default for generic timestamp value.
19
21
  def type_literal_generic_datetime(column)
20
22
  :timestamptz
@@ -0,0 +1,72 @@
1
+ # frozen-string-literal: true
2
+ #
3
+ # The run_transaction_hooks extension allows for running after_commit or
4
+ # after_rollback extensions before commit or rollback. It then removes
5
+ # the hook after running it, so it will not be run twice.
6
+ #
7
+ # This extension should only be used in transactional tests where the
8
+ # transaction always rolls back, to test the behavior of the after_commit
9
+ # and after_rollback hooks. Any other usage is probably a bad idea.
10
+ #
11
+ # Example:
12
+ #
13
+ # DB.extension :run_transaction_hooks
14
+ # x = 1
15
+ # DB.transaction(rollback: :always) do
16
+ # DB.after_rollback{x = 3}
17
+ # DB.after_commit{x = 2}
18
+ #
19
+ # x # => 1
20
+ # DB.run_after_rollback_hooks
21
+ # x # => 3
22
+ # DB.run_after_commit_hooks
23
+ # x # => 2
24
+ # end
25
+ # x # => 2
26
+
27
+ #
28
+ class Sequel::Database
29
+ module RunTransactionHooks
30
+ # Run all savepoint and transaction after_commit hooks for the current transaction,
31
+ # and remove the hooks after running them.
32
+ # Options:
33
+ # :server :: The server/shard to use.
34
+ def run_after_commit_hooks(opts=OPTS)
35
+ _run_transaction_hooks(:after_commit, opts)
36
+ end
37
+
38
+ # Run all savepoint and transaction after_rollback hooks for the current transaction,
39
+ # and remove the hooks after running them.
40
+ # Options:
41
+ # :server :: The server/shard to use.
42
+ def run_after_rollback_hooks(opts=OPTS)
43
+ _run_transaction_hooks(:after_rollback, opts)
44
+ end
45
+
46
+ private
47
+
48
+ def _run_transaction_hooks(type, opts)
49
+ synchronize(opts[:server]) do |conn|
50
+ unless h = _trans(conn)
51
+ raise Sequel::Error, "Cannot call run_#{type}_hooks outside of a transaction"
52
+ end
53
+
54
+ if hooks = h[type]
55
+ hooks.each(&:call)
56
+ hooks.clear
57
+ end
58
+
59
+ if (savepoints = h[:savepoints])
60
+ savepoints.each do |savepoint|
61
+ if hooks = savepoint[type]
62
+ hooks.each(&:call)
63
+ hooks.clear
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+
71
+ register_extension(:run_transaction_hooks, RunTransactionHooks)
72
+ end
@@ -49,7 +49,9 @@ module Sequel::S
49
49
  Sequel.expr(*a, &block)
50
50
  end
51
51
 
52
+ # :nocov:
52
53
  if RUBY_VERSION >= '2.0.0'
54
+ # :nocov:
53
55
  refine Object do
54
56
  include Sequel::S
55
57
  end
@@ -199,12 +199,18 @@ END_MIG
199
199
  end
200
200
  type = col_opts.delete(:type)
201
201
  col_opts.delete(:size) if col_opts[:size].nil?
202
- col_opts[:default] = if schema[:ruby_default].nil?
203
- column_schema_to_ruby_default_fallback(schema[:default], options)
202
+ if schema[:generated]
203
+ if options[:same_db] && database_type == :postgres
204
+ col_opts[:generated_always_as] = column_schema_to_ruby_default_fallback(schema[:default], options)
205
+ end
204
206
  else
205
- schema[:ruby_default]
207
+ col_opts[:default] = if schema[:ruby_default].nil?
208
+ column_schema_to_ruby_default_fallback(schema[:default], options)
209
+ else
210
+ schema[:ruby_default]
211
+ end
212
+ col_opts.delete(:default) if col_opts[:default].nil?
206
213
  end
207
- col_opts.delete(:default) if col_opts[:default].nil?
208
214
  col_opts[:null] = false if schema[:allow_null] == false
209
215
  if table = schema[:table]
210
216
  [:key, :on_delete, :on_update, :deferrable].each{|f| col_opts[f] = schema[f] if schema[f]}
@@ -143,13 +143,13 @@ module Sequel
143
143
 
144
144
  # Make the given server the new default server for the current thread.
145
145
  def set_default_server(default_server, read_only_server=default_server)
146
- sync{(@default_servers[Thread.current] ||= [])} << [default_server, read_only_server]
146
+ sync{(@default_servers[Sequel.current] ||= [])} << [default_server, read_only_server]
147
147
  end
148
148
 
149
149
  # Remove the current default server for the current thread, restoring the
150
150
  # previous default server.
151
151
  def clear_default_server
152
- t = Thread.current
152
+ t = Sequel.current
153
153
  a = sync{@default_servers[t]}
154
154
  a.pop
155
155
  sync{@default_servers.delete(t)} if a.empty?
@@ -157,7 +157,7 @@ module Sequel
157
157
 
158
158
  # Use the server given to with_server for the given thread, if appropriate.
159
159
  def pick_server(server)
160
- a = sync{@default_servers[Thread.current]}
160
+ a = sync{@default_servers[Sequel.current]}
161
161
  if !a || a.empty?
162
162
  super
163
163
  else
@@ -25,7 +25,9 @@
25
25
  #
26
26
  # Related module: Sequel::SymbolAref
27
27
 
28
+ # :nocov:
28
29
  raise(Sequel::Error, "Refinements require ruby 2.0.0 or greater") unless RUBY_VERSION >= '2.0.0'
30
+ # :nocov:
29
31
 
30
32
  module Sequel::SymbolAref
31
33
  refine Symbol do
@@ -23,7 +23,9 @@
23
23
  #
24
24
  # Related module: Sequel::SymbolAs
25
25
 
26
+ # :nocov:
26
27
  raise(Sequel::Error, "Refinements require ruby 2.0.0 or greater") unless RUBY_VERSION >= '2.0.0'
28
+ # :nocov:
27
29
 
28
30
  module Sequel::SymbolAs
29
31
  refine Symbol do
@@ -53,7 +53,13 @@ module Sequel
53
53
  # is given, it is used directly as the node or transition. Otherwise
54
54
  # a node is created for the current object.
55
55
  def dot(label, j=nil)
56
- @dot << "#{j||@i} [label=#{label.to_s.inspect}];"
56
+ label = case label
57
+ when nil
58
+ "<nil>"
59
+ else
60
+ label.to_s
61
+ end
62
+ @dot << "#{j||@i} [label=#{label.inspect}];"
57
63
  end
58
64
 
59
65
  # Recursive method that parses all of Sequel's internal datastructures,
@@ -61,7 +67,7 @@ module Sequel
61
67
  # structure.
62
68
  def v(e, l)
63
69
  @i += 1
64
- dot(l, "#{@stack.last} -> #{@i}") if l
70
+ dot(l, "#{@stack.last} -> #{@i}")
65
71
  @stack.push(@i)
66
72
  case e
67
73
  when LiteralString
@@ -144,7 +150,7 @@ module Sequel
144
150
  dot "Dataset"
145
151
  TO_DOT_OPTIONS.each do |k|
146
152
  if val = e.opts[k]
147
- v(val, k.to_s)
153
+ v(val, k)
148
154
  end
149
155
  end
150
156
  else
@@ -69,7 +69,9 @@ module Sequel
69
69
  require_relative "model/base"
70
70
  require_relative "model/exceptions"
71
71
  require_relative "model/errors"
72
+ # :nocov:
72
73
  if !defined?(::SEQUEL_NO_ASSOCIATIONS) && !ENV.has_key?('SEQUEL_NO_ASSOCIATIONS')
74
+ # :nocov:
73
75
  require_relative 'model/associations'
74
76
  plugin Model::Associations
75
77
  end
@@ -164,11 +164,11 @@ module Sequel
164
164
  # range to return the object(s) at the correct offset/limit.
165
165
  def apply_ruby_eager_limit_strategy(rows, limit_and_offset = limit_and_offset())
166
166
  name = self[:name]
167
+ return unless range = slice_range(limit_and_offset)
167
168
  if returns_array?
168
- range = slice_range(limit_and_offset)
169
169
  rows.each{|o| o.associations[name] = o.associations[name][range] || []}
170
- elsif sr = slice_range(limit_and_offset)
171
- offset = sr.begin
170
+ else
171
+ offset = range.begin
172
172
  rows.each{|o| o.associations[name] = o.associations[name][offset]}
173
173
  end
174
174
  end
@@ -356,7 +356,7 @@ module Sequel
356
356
  def finalize
357
357
  return unless cache = self[:cache]
358
358
 
359
- finalize_settings.each do |meth, key|
359
+ finalizer = proc do |meth, key|
360
360
  next if has_key?(key)
361
361
 
362
362
  # Allow calling private methods to make sure caching is done appropriately
@@ -364,6 +364,13 @@ module Sequel
364
364
  self[key] = cache.delete(key) if cache.has_key?(key)
365
365
  end
366
366
 
367
+ finalize_settings.each(&finalizer)
368
+
369
+ unless self[:instance_specific]
370
+ finalizer.call(:associated_eager_dataset, :associated_eager_dataset)
371
+ finalizer.call(:filter_by_associations_conditions_dataset, :filter_by_associations_conditions_dataset)
372
+ end
373
+
367
374
  nil
368
375
  end
369
376
 
@@ -371,9 +378,7 @@ module Sequel
371
378
  FINALIZE_SETTINGS = {
372
379
  :associated_class=>:class,
373
380
  :associated_dataset=>:_dataset,
374
- :associated_eager_dataset=>:associated_eager_dataset,
375
381
  :eager_limit_strategy=>:_eager_limit_strategy,
376
- :filter_by_associations_conditions_dataset=>:filter_by_associations_conditions_dataset,
377
382
  :placeholder_loader=>:placeholder_loader,
378
383
  :predicate_key=>:predicate_key,
379
384
  :predicate_keys=>:predicate_keys,
@@ -432,7 +437,11 @@ module Sequel
432
437
  if use_placeholder_loader?
433
438
  cached_fetch(:placeholder_loader) do
434
439
  Sequel::Dataset::PlaceholderLiteralizer.loader(associated_dataset) do |pl, ds|
435
- ds.where(Sequel.&(*predicate_keys.map{|k| SQL::BooleanExpression.new(:'=', k, pl.arg)}))
440
+ ds = ds.where(Sequel.&(*predicate_keys.map{|k| SQL::BooleanExpression.new(:'=', k, pl.arg)}))
441
+ if self[:block]
442
+ ds = self[:block].call(ds)
443
+ end
444
+ ds
436
445
  end
437
446
  end
438
447
  end
@@ -796,7 +805,7 @@ module Sequel
796
805
 
797
806
  # Whether the placeholder loader can be used to load the association.
798
807
  def use_placeholder_loader?
799
- !self[:instance_specific] && !self[:eager_graph]
808
+ self[:use_placeholder_loader]
800
809
  end
801
810
  end
802
811
 
@@ -1244,7 +1253,9 @@ module Sequel
1244
1253
  else
1245
1254
  assoc_record.values.delete(left_key_alias)
1246
1255
  end
1247
- next unless objects = h[hash_key]
1256
+
1257
+ objects = h[hash_key]
1258
+
1248
1259
  if assign_singular
1249
1260
  objects.each do |object|
1250
1261
  object.associations[name] ||= assoc_record
@@ -1791,11 +1802,12 @@ module Sequel
1791
1802
  opts.merge!(:type => type, :name => name, :cache=>({} if cache_associations), :model => self)
1792
1803
 
1793
1804
  opts[:block] = block if block
1794
- if !opts.has_key?(:instance_specific) && (block || orig_opts[:block] || orig_opts[:dataset])
1805
+ opts[:instance_specific] = true if orig_opts[:dataset]
1806
+ if !opts.has_key?(:instance_specific) && (block || orig_opts[:block])
1795
1807
  # It's possible the association is instance specific, in that it depends on
1796
1808
  # values other than the foreign key value. This needs to be checked for
1797
1809
  # in certain places to disable optimizations.
1798
- opts[:instance_specific] = true
1810
+ opts[:instance_specific] = _association_instance_specific_default(name)
1799
1811
  end
1800
1812
  opts = assoc_class.new.merge!(opts)
1801
1813
 
@@ -1803,6 +1815,7 @@ module Sequel
1803
1815
  raise(Error, "cannot clone an association to an association of different type (association #{name} with type #{type} cloning #{opts[:clone]} with type #{cloned_assoc[:type]})")
1804
1816
  end
1805
1817
 
1818
+ opts[:use_placeholder_loader] = !opts[:instance_specific] && !opts[:eager_graph]
1806
1819
  opts[:eager_block] = opts[:block] unless opts.include?(:eager_block)
1807
1820
  opts[:graph_join_type] ||= :left_outer
1808
1821
  opts[:order_eager_graph] = true unless opts.include?(:order_eager_graph)
@@ -1899,6 +1912,12 @@ module Sequel
1899
1912
  Plugins.def_dataset_methods(self, [:eager, :eager_graph, :eager_graph_with_options, :association_join, :association_full_join, :association_inner_join, :association_left_join, :association_right_join])
1900
1913
 
1901
1914
  private
1915
+
1916
+ # The default value for the instance_specific option, if the association
1917
+ # could be instance specific and the :instance_specific option is not specified.
1918
+ def _association_instance_specific_default(_)
1919
+ true
1920
+ end
1902
1921
 
1903
1922
  # The module to use for the association's methods. Defaults to
1904
1923
  # the overridable_methods_module.
@@ -1948,10 +1967,8 @@ module Sequel
1948
1967
  if opts[:block]
1949
1968
  opts[:block_method] = Plugins.def_sequel_method(association_module(opts), "#{opts[:name]}_block", 1, &opts[:block])
1950
1969
  end
1951
- if opts[:dataset]
1952
- opts[:dataset_opt_arity] = opts[:dataset].arity == 0 ? 0 : 1
1953
- opts[:dataset_opt_method] = Plugins.def_sequel_method(association_module(opts), "#{opts[:name]}_dataset_opt", opts[:dataset_opt_arity], &opts[:dataset])
1954
- end
1970
+ opts[:dataset_opt_arity] = opts[:dataset].arity == 0 ? 0 : 1
1971
+ opts[:dataset_opt_method] = Plugins.def_sequel_method(association_module(opts), "#{opts[:name]}_dataset_opt", opts[:dataset_opt_arity], &opts[:dataset])
1955
1972
  def_association_method(opts)
1956
1973
 
1957
1974
  return if opts[:read_only]
@@ -2122,9 +2139,7 @@ module Sequel
2122
2139
 
2123
2140
  eager_load_results(opts, eo) do |assoc_record|
2124
2141
  hash_key = uses_cks ? pk_meths.map{|k| assoc_record.get_column_value(k)} : assoc_record.get_column_value(opts.primary_key_method)
2125
- if objects = h[hash_key]
2126
- objects.each{|object| object.associations[name] = assoc_record}
2127
- end
2142
+ h[hash_key].each{|object| object.associations[name] = assoc_record}
2128
2143
  end
2129
2144
  end
2130
2145
 
@@ -2171,7 +2186,7 @@ module Sequel
2171
2186
  eager_load_results(opts, eo) do |assoc_record|
2172
2187
  assoc_record.values.delete(delete_rn) if delete_rn
2173
2188
  hash_key = uses_cks ? km.map{|k| assoc_record.get_column_value(k)} : assoc_record.get_column_value(km)
2174
- next unless objects = h[hash_key]
2189
+ objects = h[hash_key]
2175
2190
  if assign_singular
2176
2191
  objects.each do |object|
2177
2192
  unless object.associations[name]
@@ -2966,8 +2981,8 @@ module Sequel
2966
2981
  # dataset. If that association also has dependent associations, instead of a callable object,
2967
2982
  # use a hash with the callable object being the key, and the dependent association(s) as the value.
2968
2983
  #
2969
- # You can specify an alias by providing a Sequel::SQL::AliasedExpression object instead of
2970
- # an a Symbol for the assocation name.
2984
+ # You can specify an custom alias and/or join type on a per-association basis by providing an
2985
+ # Sequel::SQL::AliasedExpression object instead of an a Symbol for the association name.
2971
2986
  #
2972
2987
  # Examples:
2973
2988
  #
@@ -2983,6 +2998,14 @@ module Sequel
2983
2998
  # # FROM albums
2984
2999
  # # LEFT OUTER JOIN artists AS a ON (a.id = albums.artist_id)
2985
3000
  #
3001
+ # # For each album, eager_graph load the artist, using a specified alias
3002
+ # # and custom join type
3003
+ #
3004
+ # Album.eager_graph(Sequel[:artist].as(:a, join_type: :inner)).all
3005
+ # # SELECT ...
3006
+ # # FROM albums
3007
+ # # INNER JOIN artists AS a ON (a.id = albums.artist_id)
3008
+ #
2986
3009
  # # For each album, eager_graph load the artist and genre
2987
3010
  # Album.eager_graph(:artist, :genre).all
2988
3011
  # Album.eager_graph(:artist).eager_graph(:genre).all
@@ -3056,6 +3079,8 @@ module Sequel
3056
3079
  # significantly slower in some cases (perhaps even the majority of cases), so you should
3057
3080
  # only use this if you have benchmarked that it is faster for your use cases.
3058
3081
  def eager_graph_with_options(associations, opts=OPTS)
3082
+ return self if associations.empty?
3083
+
3059
3084
  opts = opts.dup unless opts.frozen?
3060
3085
  associations = [associations] unless associations.is_a?(Array)
3061
3086
  ds = if eg = @opts[:eager_graph]
@@ -3125,11 +3150,16 @@ module Sequel
3125
3150
  # ta :: table_alias used for the parent association
3126
3151
  # requirements :: an array, used as a stack for requirements
3127
3152
  # r :: association reflection for the current association, or an SQL::AliasedExpression
3128
- # with the reflection as the expression and the alias base as the aliaz.
3153
+ # with the reflection as the expression, the alias base as the alias (or nil to
3154
+ # use the default alias), and an optional hash with a :join_type entry as the columns
3155
+ # to use a custom join type.
3129
3156
  # *associations :: any associations dependent on this one
3130
3157
  def eager_graph_association(ds, model, ta, requirements, r, *associations)
3131
3158
  if r.is_a?(SQL::AliasedExpression)
3132
3159
  alias_base = r.alias
3160
+ if r.columns.is_a?(Hash)
3161
+ join_type = r.columns[:join_type]
3162
+ end
3133
3163
  r = r.expression
3134
3164
  else
3135
3165
  alias_base = r[:graph_alias_base]
@@ -3152,7 +3182,7 @@ module Sequel
3152
3182
  raise Error, "Cannot eager_graph association when :conditions specified and not a hash or an array of pairs. Specify :graph_conditions, :graph_only_conditions, or :graph_block for the association. Model: #{r[:model]}, association: #{r[:name]}"
3153
3183
  end
3154
3184
 
3155
- ds = loader.call(:self=>ds, :table_alias=>assoc_table_alias, :implicit_qualifier=>(ta == ds.opts[:eager_graph][:master]) ? first_source : qualifier_from_alias_symbol(ta, first_source), :callback=>callback, :join_type=>local_opts[:join_type], :join_only=>local_opts[:join_only], :limit_strategy=>limit_strategy, :from_self_alias=>ds.opts[:eager_graph][:master])
3185
+ ds = loader.call(:self=>ds, :table_alias=>assoc_table_alias, :implicit_qualifier=>(ta == ds.opts[:eager_graph][:master]) ? first_source : qualifier_from_alias_symbol(ta, first_source), :callback=>callback, :join_type=>join_type || local_opts[:join_type], :join_only=>local_opts[:join_only], :limit_strategy=>limit_strategy, :from_self_alias=>ds.opts[:eager_graph][:master])
3156
3186
  if r[:order_eager_graph] && (order = r.fetch(:graph_order, r[:order]))
3157
3187
  ds = ds.order_append(*qualified_expression(order, assoc_table_alias))
3158
3188
  end
@@ -3177,7 +3207,6 @@ module Sequel
3177
3207
  # requirements :: an array, used as a stack for requirements
3178
3208
  # *associations :: the associations to add to the graph
3179
3209
  def eager_graph_associations(ds, model, ta, requirements, *associations)
3180
- return ds if associations.empty?
3181
3210
  associations.flatten.each do |association|
3182
3211
  ds = case association
3183
3212
  when Symbol, SQL::AliasedExpression
@@ -3307,7 +3336,7 @@ module Sequel
3307
3336
  end
3308
3337
  end
3309
3338
 
3310
- SQL::AliasedExpression.new(check_association(model, expr), association.alias)
3339
+ SQL::AliasedExpression.new(check_association(model, expr), association.alias || expr, association.columns)
3311
3340
  else
3312
3341
  check_association(model, association)
3313
3342
  end