sequel 5.43.0 → 5.44.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.
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