sequel 5.30.0 → 5.35.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 (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