sequel 5.43.0 → 5.44.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 69c695b559f3d5c19284905e8bad5a8775cced221f5cd3f5bb1d89daa9c0785c
4
- data.tar.gz: 03cd2d01139038c3e6d1e1232f6b48a104657391aeefd82feab2636044480eba
3
+ metadata.gz: 84b971e8db174387861ce5ccbb36f43ba2ffa39a8b5f88359c1246eac5ff1539
4
+ data.tar.gz: d90034f07d752fc45f6359d9b38708e865200c1a1f0c9a1ea77c9d24d681e0a3
5
5
  SHA512:
6
- metadata.gz: 4b6f0b096657603c11cf54b1b15b660c5b85a697e8b31b69f97f33d579401a73915a315b771467e56159313243c5643bb3ca5fd8c208f2c443d8c7536cba9fc0
7
- data.tar.gz: 1a44276ab427bc2f9a82e2f651950100360df998af75a9d3ca08618acfa3778b02764f544738569c5947b350ac8b2485b825052869ac882728707014e90633a8
6
+ metadata.gz: 194b8b093ecc8c18b228a19b1c27b21535590de8ac5d5002af1038ee28add4a8afdf31741bd33a5838c94535ec272cbb594e6674d76dcdd99518f5e3cbca4dda
7
+ data.tar.gz: cb54772e671e6c82422e6f4b076b3c7748165837d3501834ddc374cf939c4ea08cc581876150be72c3532a0f3591aaef18e4858e00493db0b49724a1257da957
data/CHANGELOG CHANGED
@@ -1,3 +1,13 @@
1
+ === 5.44.0 (2021-05-01)
2
+
3
+ * Add concurrent_eager_loading plugin, for eager loading multiple associations concurrently using separate threads (jeremyevans)
4
+
5
+ * Support :weeks as a interval unit in the date_arithmetic extension (jeremyevans) (#1759)
6
+
7
+ * Raise an exception if an interval hash with an unsupported key is passed in the date_arithmetic extension (jeremyevans) (#1759)
8
+
9
+ * Support dropping non-composite unique constraints on SQLite (jeremyevans) (#1755)
10
+
1
11
  === 5.43.0 (2021-04-01)
2
12
 
3
13
  * Add column_encryption plugin, for encrypting column values (jeremyevans)
@@ -0,0 +1,32 @@
1
+ = New Features
2
+
3
+ * A concurrent_eager_loading plugin has been added. This plugin
4
+ builds on top of the async_thread_pool Database extension and
5
+ allows eager loading multiple associations concurrently in
6
+ separate threads. With this plugin, you can mark datasets for
7
+ concurrent eager loading using eager_load_concurrently:
8
+
9
+ Album.eager_load_concurrently.eager(:artist, :genre, :tracks).all
10
+
11
+ Datasets that are marked for concurrent eager loading will use
12
+ concurrent eager loading if they are eager loading more than one
13
+ association. If you would like to make concurrent eager loading
14
+ the default, you can load the plugin with the :always option.
15
+
16
+ All of the association types that ship with Sequel now support
17
+ concurrent eager loading when using this plugin. For custom eager
18
+ loaders using the :eager_loader association option, please see the
19
+ documentation for the plugin for how to enable custom eager loading
20
+ for them.
21
+
22
+ = Other Improvements
23
+
24
+ * The date_arithmetic extension now handles ActiveSupport::Duration
25
+ values with weeks, as well as :weeks as a key in a hash value. Weeks
26
+ are converted into 7 days internally.
27
+
28
+ * The shared SQLite adapter now emulates the dropping of non-composite
29
+ unique constraints. Non-composite unique constraints are now
30
+ treated similarly to composite unique constraints, in that dropping
31
+ any unique constraints on a table will drop all unique constraints
32
+ on that table.
data/doc/testing.rdoc CHANGED
@@ -162,6 +162,7 @@ SEQUEL_ASYNC_THREAD_POOL_PREEMPT :: Use the async_thread_pool extension when run
162
162
  SEQUEL_COLUMNS_INTROSPECTION :: Use the columns_introspection extension when running the specs
163
163
  SEQUEL_CONNECTION_VALIDATOR :: Use the connection validator extension when running the specs
164
164
  SEQUEL_DUPLICATE_COLUMNS_HANDLER :: Use the duplicate columns handler extension with value given when running the specs
165
+ SEQUEL_CONCURRENT_EAGER_LOADING :: Use the async_thread_pool extension and concurrent_eager_loading plugin when running the specs
165
166
  SEQUEL_ERROR_SQL :: Use the error_sql extension when running the specs
166
167
  SEQUEL_INDEX_CACHING :: Use the index_caching extension when running the specs
167
168
  SEQUEL_FIBER_CONCURRENCY :: Use the fiber_concurrency extension when running the adapter and integration specs
@@ -424,10 +424,10 @@ module Sequel
424
424
  skip_indexes = []
425
425
  indexes(table, :only_autocreated=>true).each do |name, h|
426
426
  skip_indexes << name
427
- if h[:unique]
427
+ if h[:unique] && !opts[:no_unique]
428
428
  if h[:columns].length == 1
429
429
  unique_columns.concat(h[:columns])
430
- elsif h[:columns].map(&:to_s) != pks && !opts[:no_unique]
430
+ elsif h[:columns].map(&:to_s) != pks
431
431
  constraints << {:type=>:unique, :columns=>h[:columns]}
432
432
  end
433
433
  end
data/lib/sequel/core.rb CHANGED
@@ -176,6 +176,17 @@ module Sequel
176
176
  JSON.parse(json, :create_additions=>false)
177
177
  end
178
178
 
179
+ # If a mutex is given, synchronize access using it. If nil is given, just
180
+ # yield to the block. This is designed for cases where a mutex may or may
181
+ # not be provided.
182
+ def synchronize_with(mutex)
183
+ if mutex
184
+ mutex.synchronize{yield}
185
+ else
186
+ yield
187
+ end
188
+ end
189
+
179
190
  # Convert each item in the array to the correct type, handling multi-dimensional
180
191
  # arrays. For each element in the array or subarrays, call the converter,
181
192
  # unless the value is nil.
@@ -214,14 +214,12 @@ module Sequel
214
214
  end
215
215
 
216
216
  # Add a full text index on the given columns.
217
+ # See #index for additional options.
217
218
  #
218
219
  # PostgreSQL specific options:
219
220
  # :index_type :: Can be set to :gist to use a GIST index instead of the
220
221
  # default GIN index.
221
222
  # :language :: Set a language to use for the index (default: simple).
222
- #
223
- # Microsoft SQL Server specific options:
224
- # :key_index :: The KEY INDEX to use for the full text index.
225
223
  def full_text_index(columns, opts = OPTS)
226
224
  index(columns, opts.merge(:type => :full_text))
227
225
  end
@@ -231,35 +229,43 @@ module Sequel
231
229
  columns.any?{|c| c[:name] == name}
232
230
  end
233
231
 
234
- # Add an index on the given column(s) with the given options.
232
+ # Add an index on the given column(s) with the given options. Examples:
233
+ #
234
+ # index :name
235
+ # # CREATE INDEX table_name_index ON table (name)
236
+ #
237
+ # index [:artist_id, :name]
238
+ # # CREATE INDEX table_artist_id_name_index ON table (artist_id, name)
239
+ #
240
+ # index [:artist_id, :name], name: :foo
241
+ # # CREATE INDEX foo ON table (artist_id, name)
242
+ #
235
243
  # General options:
236
244
  #
245
+ # :include :: Include additional column values in the index, without
246
+ # actually indexing on those values (only supported by
247
+ # some databases).
237
248
  # :name :: The name to use for the index. If not given, a default name
238
249
  # based on the table and columns is used.
239
- # :type :: The type of index to use (only supported by some databases)
250
+ # :type :: The type of index to use (only supported by some databases,
251
+ # :full_text and :spatial values are handled specially).
240
252
  # :unique :: Make the index unique, so duplicate values are not allowed.
241
- # :where :: Create a partial index (only supported by some databases)
253
+ # :where :: A filter expression, used to create a partial index (only
254
+ # supported by some databases).
242
255
  #
243
256
  # PostgreSQL specific options:
244
257
  #
245
258
  # :concurrently :: Create the index concurrently, so it doesn't block
246
259
  # operations on the table while the index is being
247
260
  # built.
248
- # :opclass :: Use a specific operator class in the index.
249
- # :include :: Include additional column values in the index, without
250
- # actually indexing on those values (PostgreSQL 11+).
261
+ # :if_not_exists :: Only create the index if an index of the same name doesn't already exist.
262
+ # :opclass :: Set an opclass to use for all columns (per-column opclasses require
263
+ # custom SQL).
251
264
  # :tablespace :: Specify tablespace for index.
252
265
  #
253
266
  # Microsoft SQL Server specific options:
254
267
  #
255
- # :include :: Include additional column values in the index, without
256
- # actually indexing on those values.
257
- #
258
- # index :name
259
- # # CREATE INDEX table_name_index ON table (name)
260
- #
261
- # index [:artist_id, :name]
262
- # # CREATE INDEX table_artist_id_name_index ON table (artist_id, name)
268
+ # :key_index :: Sets the KEY INDEX to the given value.
263
269
  def index(columns, opts = OPTS)
264
270
  indexes << {:columns => Array(columns)}.merge!(opts)
265
271
  nil
@@ -325,6 +331,7 @@ module Sequel
325
331
  end
326
332
 
327
333
  # Add a spatial index on the given columns.
334
+ # See #index for additional options.
328
335
  def spatial_index(columns, opts = OPTS)
329
336
  index(columns, opts.merge(:type => :spatial))
330
337
  end
@@ -451,7 +458,7 @@ module Sequel
451
458
  end
452
459
 
453
460
  # Add a full text index on the given columns.
454
- # See CreateTableGenerator#index for available options.
461
+ # See CreateTableGenerator#full_text_index for available options.
455
462
  def add_full_text_index(columns, opts = OPTS)
456
463
  add_index(columns, {:type=>:full_text}.merge!(opts))
457
464
  end
@@ -460,34 +467,6 @@ module Sequel
460
467
  # CreateTableGenerator#index for available options.
461
468
  #
462
469
  # add_index(:artist_id) # CREATE INDEX table_artist_id_index ON table (artist_id)
463
- #
464
- # Options:
465
- #
466
- # :name :: Give a specific name for the index. Highly recommended if you plan on
467
- # dropping the index later.
468
- # :where :: A filter expression, used to setup a partial index (if supported).
469
- # :unique :: Create a unique index.
470
- #
471
- # PostgreSQL specific options:
472
- #
473
- # :concurrently :: Create the index concurrently, so it doesn't require an exclusive lock
474
- # on the table.
475
- # :index_type :: The underlying index type to use for a full_text index, gin by default).
476
- # :language :: The language to use for a full text index (simple by default).
477
- # :opclass :: Set an opclass to use for all columns (per-column opclasses require
478
- # custom SQL).
479
- # :type :: Set the index type (e.g. full_text, spatial, hash, gin, gist, btree).
480
- # :if_not_exists :: Only create the index if an index of the same name doesn't already exists
481
- #
482
- # MySQL specific options:
483
- #
484
- # :type :: Set the index type, with full_text and spatial indexes handled specially.
485
- #
486
- # Microsoft SQL Server specific options:
487
- #
488
- # :include :: Includes additional columns in the index.
489
- # :key_index :: Sets the KEY INDEX to the given value.
490
- # :type :: clustered uses a clustered index, full_text uses a full text index.
491
470
  def add_index(columns, opts = OPTS)
492
471
  @operations << {:op => :add_index, :columns => Array(columns)}.merge!(opts)
493
472
  nil
@@ -8,9 +8,10 @@
8
8
  # DB.extension :date_arithmetic
9
9
  #
10
10
  # Then you can use the Sequel.date_add and Sequel.date_sub methods
11
- # to return Sequel expressions:
11
+ # to return Sequel expressions (this example shows the only supported
12
+ # keys for the second argument):
12
13
  #
13
- # add = Sequel.date_add(:date_column, years: 1, months: 2, days: 3)
14
+ # add = Sequel.date_add(:date_column, years: 1, months: 2, weeks: 2, days: 1)
14
15
  # sub = Sequel.date_sub(:date_column, hours: 1, minutes: 2, seconds: 3)
15
16
  #
16
17
  # In addition to specifying the interval as a hash, there is also
@@ -184,22 +185,35 @@ module Sequel
184
185
  # ActiveSupport::Duration :: Converted to a hash using the interval's parts.
185
186
  def initialize(expr, interval, opts=OPTS)
186
187
  @expr = expr
187
- @interval = if interval.is_a?(Hash)
188
- interval.each_value do |v|
189
- # Attempt to prevent SQL injection by users who pass untrusted strings
190
- # as interval values.
191
- if v.is_a?(String) && !v.is_a?(LiteralString)
192
- raise Sequel::InvalidValue, "cannot provide String value as interval part: #{v.inspect}"
193
- end
188
+
189
+ h = Hash.new(0)
190
+ interval = interval.parts unless interval.is_a?(Hash)
191
+ interval.each do |unit, value|
192
+ # skip nil values
193
+ next unless value
194
+
195
+ # Convert weeks to days, as ActiveSupport::Duration can use weeks,
196
+ # but the database-specific literalizers only support days.
197
+ if unit == :weeks
198
+ unit = :days
199
+ value *= 7
200
+ end
201
+
202
+ unless DatasetMethods::DURATION_UNITS.include?(unit)
203
+ raise Sequel::Error, "Invalid key used in DateAdd interval hash: #{unit.inspect}"
194
204
  end
195
- Hash[interval]
196
- else
197
- h = Hash.new(0)
198
- interval.parts.each{|unit, value| h[unit] += value}
199
- Hash[h]
205
+
206
+ # Attempt to prevent SQL injection by users who pass untrusted strings
207
+ # as interval values. It doesn't make sense to support literal strings,
208
+ # due to the numeric adding below.
209
+ if value.is_a?(String)
210
+ raise Sequel::InvalidValue, "cannot provide String value as interval part: #{value.inspect}"
211
+ end
212
+
213
+ h[unit] += value
200
214
  end
201
215
 
202
- @interval.freeze
216
+ @interval = Hash[h].freeze
203
217
  @cast_type = opts[:cast] if opts[:cast]
204
218
  freeze
205
219
  end
@@ -42,7 +42,7 @@
42
42
  #
43
43
  # This extension integrates with the pg_array extension. If you plan
44
44
  # to use arrays of enum types, load the pg_array extension before the
45
- # pg_interval extension:
45
+ # pg_enum extension:
46
46
  #
47
47
  # DB.extension :pg_array, :pg_enum
48
48
  #
@@ -263,7 +263,9 @@ module Sequel
263
263
  # yielding each row to the block.
264
264
  def eager_load_results(eo, &block)
265
265
  rows = eo[:rows]
266
- initialize_association_cache(rows) unless eo[:initialize_rows] == false
266
+ unless eo[:initialize_rows] == false
267
+ Sequel.synchronize_with(eo[:mutex]){initialize_association_cache(rows)}
268
+ end
267
269
  if eo[:id_map]
268
270
  ids = eo[:id_map].keys
269
271
  return ids if ids.empty?
@@ -311,7 +313,8 @@ module Sequel
311
313
  objects = loader.all(ids)
312
314
  end
313
315
 
314
- objects.each(&block)
316
+ Sequel.synchronize_with(eo[:mutex]){objects.each(&block)}
317
+
315
318
  if strategy == :ruby
316
319
  apply_ruby_eager_limit_strategy(rows, eager_limit || limit_and_offset)
317
320
  end
@@ -3374,15 +3377,30 @@ module Sequel
3374
3377
  egl.dup
3375
3378
  end
3376
3379
 
3377
- # Eagerly load all specified associations
3380
+ # Eagerly load all specified associations.
3378
3381
  def eager_load(a, eager_assoc=@opts[:eager])
3379
3382
  return if a.empty?
3383
+
3384
+ # Reflections for all associations to eager load
3385
+ reflections = eager_assoc.keys.map{|assoc| model.association_reflection(assoc) || (raise Sequel::UndefinedAssociation, "Model: #{self}, Association: #{assoc}")}
3386
+
3387
+ perform_eager_loads(prepare_eager_load(a, reflections, eager_assoc))
3388
+
3389
+ reflections.each do |r|
3390
+ a.each{|object| object.send(:run_association_callbacks, r, :after_load, object.associations[r[:name]])} if r[:after_load]
3391
+ end
3392
+
3393
+ nil
3394
+ end
3395
+
3396
+ # Prepare a hash loaders and eager options which will be used to implement the eager loading.
3397
+ def prepare_eager_load(a, reflections, eager_assoc)
3398
+ eager_load_data = {}
3399
+
3380
3400
  # Key is foreign/primary key name symbol.
3381
3401
  # Value is hash with keys being foreign/primary key values (generally integers)
3382
3402
  # and values being an array of current model objects with that specific foreign/primary key
3383
3403
  key_hash = {}
3384
- # Reflections for all associations to eager load
3385
- reflections = eager_assoc.keys.map{|assoc| model.association_reflection(assoc) || (raise Sequel::UndefinedAssociation, "Model: #{self}, Association: #{assoc}")}
3386
3404
 
3387
3405
  # Populate the key_hash entry for each association being eagerly loaded
3388
3406
  reflections.each do |r|
@@ -3413,7 +3431,6 @@ module Sequel
3413
3431
  id_map = nil
3414
3432
  end
3415
3433
 
3416
- loader = r[:eager_loader]
3417
3434
  associations = eager_assoc[r[:name]]
3418
3435
  if associations.respond_to?(:call)
3419
3436
  eager_block = associations
@@ -3421,9 +3438,23 @@ module Sequel
3421
3438
  elsif associations.is_a?(Hash) && associations.length == 1 && (pr_assoc = associations.to_a.first) && pr_assoc.first.respond_to?(:call)
3422
3439
  eager_block, associations = pr_assoc
3423
3440
  end
3424
- loader.call(:key_hash=>key_hash, :rows=>a, :associations=>associations, :self=>self, :eager_block=>eager_block, :id_map=>id_map)
3425
- a.each{|object| object.send(:run_association_callbacks, r, :after_load, object.associations[r[:name]])} if r[:after_load]
3426
- end
3441
+
3442
+ eager_load_data[r[:eager_loader]] = {:key_hash=>key_hash, :rows=>a, :associations=>associations, :self=>self, :eager_block=>eager_block, :id_map=>id_map}
3443
+ end
3444
+
3445
+ eager_load_data
3446
+ end
3447
+
3448
+ # Using the hash of loaders and eager options, perform the eager loading.
3449
+ def perform_eager_loads(eager_load_data)
3450
+ eager_load_data.map do |loader, eo|
3451
+ perform_eager_load(loader, eo)
3452
+ end
3453
+ end
3454
+
3455
+ # Perform eager loading for a single association using the loader and eager options.
3456
+ def perform_eager_load(loader, eo)
3457
+ loader.call(eo)
3427
3458
  end
3428
3459
 
3429
3460
  # Return a subquery expression for filering by a many_to_many association
@@ -5,7 +5,7 @@ module Sequel
5
5
 
6
6
  module Plugins
7
7
  # The async_thread_pool plugin makes it slightly easier to use the async_thread_pool
8
- # Dataset extension with models. It makes Model.async return an async dataset for the
8
+ # Database extension with models. It makes Model.async return an async dataset for the
9
9
  # model, and support async behavior for #destroy, #with_pk, and #with_pk! for model
10
10
  # datasets:
11
11
  #
@@ -7,10 +7,27 @@ raise(Sequel::Error, "Sequel column_encryption plugin requires ruby 2.3 or great
7
7
  require 'openssl'
8
8
 
9
9
  begin
10
- OpenSSL::Cipher.new("aes-256-gcm")
11
- rescue OpenSSL::Cipher::CipherError
10
+ # Test cipher actually works
11
+ cipher = OpenSSL::Cipher.new("aes-256-gcm")
12
+ cipher.encrypt
13
+ cipher.key = '1'*32
14
+ cipher_iv = cipher.random_iv
15
+ cipher.auth_data = ''
16
+ cipher_text = cipher.update('2') << cipher.final
17
+ auth_tag = cipher.auth_tag
18
+
19
+ cipher = OpenSSL::Cipher.new("aes-256-gcm")
20
+ cipher.decrypt
21
+ cipher.iv = cipher_iv
22
+ cipher.key = '1'*32
23
+ cipher.auth_data = ''
24
+ cipher.auth_tag = auth_tag
12
25
  # :nocov:
13
- raise LoadError, "Sequel column_encryption plugin requires the aes-256-gcm cipher"
26
+ unless (cipher.update(cipher_text) << cipher.final) == '2'
27
+ raise OpenSSL::Cipher::CipherError
28
+ end
29
+ rescue RuntimeError, OpenSSL::Cipher::CipherError
30
+ raise LoadError, "Sequel column_encryption plugin requires a working aes-256-gcm cipher"
14
31
  # :nocov:
15
32
  end
16
33
 
@@ -0,0 +1,174 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Sequel
4
+ extension 'async_thread_pool'
5
+
6
+ module Plugins
7
+ # The concurrent_eager_loading plugin allows for eager loading multiple associations
8
+ # concurrently in separate threads. You must load the async_thread_pool Database
9
+ # extension into the Database object the model class uses in order for this plugin
10
+ # to work.
11
+ #
12
+ # By default in Sequel, eager loading happens in a serial manner. If you have code
13
+ # such as:
14
+ #
15
+ # Album.eager(:artist, :genre, :tracks)
16
+ #
17
+ # Sequel will load the albums, then the artists for the albums, then
18
+ # the genres for the albums, then the tracks for the albums.
19
+ #
20
+ # With the concurrent_eager_loading plugin, you can use the +eager_load_concurrently+
21
+ # method to allow for concurrent eager loading:
22
+ #
23
+ # Album.eager_load_concurrently.eager(:artist, :genre, :tracks)
24
+ #
25
+ # This will load the albums, first, since it needs to load the albums to know
26
+ # which artists, genres, and tracks to eagerly load. However, it will load the
27
+ # artists, genres, and tracks for the albums concurrently in separate threads.
28
+ # This can significantly improve performance, especially if there is significant
29
+ # latency between the application and the database. Note that using separate threads
30
+ # is only used in the case where there are multiple associations to eagerly load.
31
+ # With only a single association to eagerly load, there is no reason to use a
32
+ # separate thread, since it would not improve performance.
33
+ #
34
+ # If you want to make concurrent eager loading the default, you can load the
35
+ # plugin with the +:always+ option. In this case, all eager loads will be
36
+ # concurrent. If you want to force a non-concurrent eager load, you can use
37
+ # +eager_load_serially+:
38
+ #
39
+ # Album.eager_load_serially.eager(:artist, :genre, :tracks)
40
+ #
41
+ # Note that making concurrent eager loading the default is probably a bad idea
42
+ # if you are eager loading inside transactions and want the eager load to
43
+ # reflect changes made inside the transaction, unless you plan to use
44
+ # +eager_load_serially+ for such cases. See the async_thread_pool
45
+ # Database extension documentation for more general caveats regarding its use.
46
+ #
47
+ # The default eager loaders for all of the association types that ship with Sequel
48
+ # support safe concurrent eager loading. However, if you are specifying a custom
49
+ # +:eager_loader+ for an association, it may not work safely unless it it modified to
50
+ # support concurrent eager loading. Taking this example from the
51
+ # {Advanced Associations guide}[rdoc-ref:doc/advanced_associations.rdoc]
52
+ #
53
+ # Album.many_to_one :artist, :eager_loader=>(proc do |eo_opts|
54
+ # eo_opts[:rows].each{|album| album.associations[:artist] = nil}
55
+ # id_map = eo_opts[:id_map]
56
+ # Artist.where(:id=>id_map.keys).all do |artist|
57
+ # if albums = id_map[artist.id]
58
+ # albums.each do |album|
59
+ # album.associations[:artist] = artist
60
+ # end
61
+ # end
62
+ # end
63
+ # end)
64
+ #
65
+ # This would not support concurrent eager loading safely. To support safe
66
+ # concurrent eager loading, you need to make sure you are not modifying
67
+ # the associations for objects concurrently by separate threads. This is
68
+ # implemented using a mutex, which you can access via <tt>eo_opts[:mutex]</tt>.
69
+ # To keep things simple, you can use +Sequel.synchronize_with+ to only
70
+ # use this mutex if it is available. You want to use the mutex around the
71
+ # code that initializes the associations (usually to +nil+ or <tt>[]</tt>),
72
+ # and also around the code that sets the associatied objects appropriately
73
+ # after they have been retreived. You do not want to use the mutex around
74
+ # the code that loads the objects, since that will prevent concurrent loading.
75
+ # So after the changes, the custom eager loader would look like this:
76
+ #
77
+ # Album.many_to_one :artist, :eager_loader=>(proc do |eo_opts|
78
+ # Sequel.synchronize_with(eo[:mutex]) do
79
+ # eo_opts[:rows].each{|album| album.associations[:artist] = nil}
80
+ # end
81
+ # id_map = eo_opts[:id_map]
82
+ # rows = Artist.where(:id=>id_map.keys).all
83
+ # Sequel.synchronize_with(eo[:mutex]) do
84
+ # rows.each do |artist|
85
+ # if albums = id_map[artist.id]
86
+ # albums.each do |album|
87
+ # album.associations[:artist] = artist
88
+ # end
89
+ # end
90
+ # end
91
+ # end
92
+ # end)
93
+ #
94
+ # Usage:
95
+ #
96
+ # # Make all model subclass datasets support concurrent eager loading
97
+ # Sequel::Model.plugin :concurrent_eager_loading
98
+ #
99
+ # # Make the Album class datasets support concurrent eager loading
100
+ # Album.plugin :concurrent_eager_loading
101
+ #
102
+ # # Make all model subclass datasets concurrently eager load by default
103
+ # Sequel::Model.plugin :concurrent_eager_loading, always: true
104
+ module ConcurrentEagerLoading
105
+ def self.configure(mod, opts=OPTS)
106
+ if opts.has_key?(:always)
107
+ mod.instance_variable_set(:@always_eager_load_concurrently, opts[:always])
108
+ end
109
+ end
110
+
111
+ module ClassMethods
112
+ Plugins.inherited_instance_variables(self, :@always_eager_load_concurrently => nil)
113
+ Plugins.def_dataset_methods(self, [:eager_load_concurrently, :eager_load_serially])
114
+
115
+ # Whether datasets for this class should eager load concurrently by default.
116
+ def always_eager_load_concurrently?
117
+ @always_eager_load_concurrently
118
+ end
119
+ end
120
+
121
+ module DatasetMethods
122
+ # Return a cloned dataset that will eager load associated results concurrently
123
+ # using the async thread pool.
124
+ def eager_load_concurrently
125
+ cached_dataset(:_eager_load_concurrently) do
126
+ clone(:eager_load_concurrently=>true)
127
+ end
128
+ end
129
+
130
+ # Return a cloned dataset that will noteager load associated results concurrently
131
+ # using the async thread pool. Only useful if the current dataset has been marked
132
+ # as loading concurrently, or loading concurrently is the model's default behavior.
133
+ def eager_load_serially
134
+ cached_dataset(:_eager_load_serially) do
135
+ clone(:eager_load_concurrently=>false)
136
+ end
137
+ end
138
+
139
+ private
140
+
141
+ # Whether this particular dataset will eager load results concurrently.
142
+ def eager_load_concurrently?
143
+ v = @opts[:eager_load_concurrently]
144
+ v.nil? ? model.always_eager_load_concurrently? : v
145
+ end
146
+
147
+ # If performing eager loads concurrently, and at least 2 associations are being
148
+ # eagerly loaded, create a single mutex used for all eager loads. After the
149
+ # eager loads have been performed, force loading of any async results, so that
150
+ # all eager loads will have been completed before this method returns.
151
+ def perform_eager_loads(eager_load_data)
152
+ return super if !eager_load_concurrently? || eager_load_data.length < 2
153
+
154
+ mutex = Mutex.new
155
+ eager_load_data.each_value do |eo|
156
+ eo[:mutex] = mutex
157
+ end
158
+
159
+ super.each do |v|
160
+ if Sequel::Database::AsyncThreadPool::BaseProxy === v
161
+ v.__value
162
+ end
163
+ end
164
+ end
165
+
166
+ # If performing eager loads concurrently, perform this eager load using the
167
+ # async thread pool.
168
+ def perform_eager_load(loader, eo)
169
+ eo[:mutex] ? db.send(:async_run){super} : super
170
+ end
171
+ end
172
+ end
173
+ end
174
+ end
@@ -426,10 +426,12 @@ module Sequel
426
426
  id_map = {}
427
427
  pkm = opts.primary_key_method
428
428
 
429
- rows.each do |object|
430
- if associated_pks = object.get_column_value(key)
431
- associated_pks.each do |apk|
432
- (id_map[apk] ||= []) << object
429
+ Sequel.synchronize_with(eo[:mutex]) do
430
+ rows.each do |object|
431
+ if associated_pks = object.get_column_value(key)
432
+ associated_pks.each do |apk|
433
+ (id_map[apk] ||= []) << object
434
+ end
433
435
  end
434
436
  end
435
437
  end
@@ -170,11 +170,13 @@ module Sequel
170
170
  id_map = eo[:id_map]
171
171
  parent_map = {}
172
172
  children_map = {}
173
- eo[:rows].each do |obj|
174
- parent_map[prkey_conv[obj]] = obj
175
- (children_map[key_conv[obj]] ||= []) << obj
176
- obj.associations[ancestors] = []
177
- obj.associations[parent] = nil
173
+ Sequel.synchronize_with(eo[:mutex]) do
174
+ eo[:rows].each do |obj|
175
+ parent_map[prkey_conv[obj]] = obj
176
+ (children_map[key_conv[obj]] ||= []) << obj
177
+ obj.associations[ancestors] = []
178
+ obj.associations[parent] = nil
179
+ end
178
180
  end
179
181
  r = model.association_reflection(ancestors)
180
182
  base_case = model.where(prkey=>id_map.keys).
@@ -207,10 +209,12 @@ module Sequel
207
209
  root.associations[ancestors] << obj
208
210
  end
209
211
  end
210
- parent_map.each do |parent_id, obj|
211
- if children = children_map[parent_id]
212
- children.each do |child|
213
- child.associations[parent] = obj
212
+ Sequel.synchronize_with(eo[:mutex]) do
213
+ parent_map.each do |parent_id, obj|
214
+ if children = children_map[parent_id]
215
+ children.each do |child|
216
+ child.associations[parent] = obj
217
+ end
214
218
  end
215
219
  end
216
220
  end
@@ -268,10 +272,12 @@ module Sequel
268
272
  associations = eo[:associations]
269
273
  parent_map = {}
270
274
  children_map = {}
271
- eo[:rows].each do |obj|
272
- parent_map[prkey_conv[obj]] = obj
273
- obj.associations[descendants] = []
274
- obj.associations[childrena] = []
275
+ Sequel.synchronize_with(eo[:mutex]) do
276
+ eo[:rows].each do |obj|
277
+ parent_map[prkey_conv[obj]] = obj
278
+ obj.associations[descendants] = []
279
+ obj.associations[childrena] = []
280
+ end
275
281
  end
276
282
  r = model.association_reflection(descendants)
277
283
  base_case = model.where(key=>id_map.keys).
@@ -316,12 +322,14 @@ module Sequel
316
322
 
317
323
  (children_map[key_conv[obj]] ||= []) << obj
318
324
  end
319
- children_map.each do |parent_id, objs|
320
- objs = objs.uniq
321
- parent_obj = parent_map[parent_id]
322
- parent_obj.associations[childrena] = objs
323
- objs.each do |obj|
324
- obj.associations[parent] = parent_obj
325
+ Sequel.synchronize_with(eo[:mutex]) do
326
+ children_map.each do |parent_id, objs|
327
+ objs = objs.uniq
328
+ parent_obj = parent_map[parent_id]
329
+ parent_obj.associations[childrena] = objs
330
+ objs.each do |obj|
331
+ obj.associations[parent] = parent_obj
332
+ end
325
333
  end
326
334
  end
327
335
  end
@@ -6,7 +6,7 @@ module Sequel
6
6
 
7
7
  # The minor version of Sequel. Bumped for every non-patch level
8
8
  # release, generally around once a month.
9
- MINOR = 43
9
+ MINOR = 44
10
10
 
11
11
  # The tiny version of Sequel. Usually 0, only bumped for bugfix
12
12
  # releases that fix regressions from previous versions.
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sequel
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.43.0
4
+ version: 5.44.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeremy Evans
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-04-01 00:00:00.000000000 Z
11
+ date: 2021-05-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: minitest
@@ -187,6 +187,7 @@ extra_rdoc_files:
187
187
  - doc/release_notes/5.41.0.txt
188
188
  - doc/release_notes/5.42.0.txt
189
189
  - doc/release_notes/5.43.0.txt
190
+ - doc/release_notes/5.44.0.txt
190
191
  - doc/release_notes/5.5.0.txt
191
192
  - doc/release_notes/5.6.0.txt
192
193
  - doc/release_notes/5.7.0.txt
@@ -258,6 +259,7 @@ files:
258
259
  - doc/release_notes/5.41.0.txt
259
260
  - doc/release_notes/5.42.0.txt
260
261
  - doc/release_notes/5.43.0.txt
262
+ - doc/release_notes/5.44.0.txt
261
263
  - doc/release_notes/5.5.0.txt
262
264
  - doc/release_notes/5.6.0.txt
263
265
  - doc/release_notes/5.7.0.txt
@@ -465,6 +467,7 @@ files:
465
467
  - lib/sequel/plugins/column_select.rb
466
468
  - lib/sequel/plugins/columns_updated.rb
467
469
  - lib/sequel/plugins/composition.rb
470
+ - lib/sequel/plugins/concurrent_eager_loading.rb
468
471
  - lib/sequel/plugins/constraint_validations.rb
469
472
  - lib/sequel/plugins/csv_serializer.rb
470
473
  - lib/sequel/plugins/dataset_associations.rb
@@ -566,7 +569,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
566
569
  - !ruby/object:Gem::Version
567
570
  version: '0'
568
571
  requirements: []
569
- rubygems_version: 3.2.3
572
+ rubygems_version: 3.2.15
570
573
  signing_key:
571
574
  specification_version: 4
572
575
  summary: The Database Toolkit for Ruby