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 +4 -4
- data/CHANGELOG +10 -0
- data/doc/release_notes/5.44.0.txt +32 -0
- data/doc/testing.rdoc +1 -0
- data/lib/sequel/adapters/shared/sqlite.rb +2 -2
- data/lib/sequel/core.rb +11 -0
- data/lib/sequel/database/schema_generator.rb +25 -46
- data/lib/sequel/extensions/date_arithmetic.rb +29 -15
- data/lib/sequel/extensions/pg_enum.rb +1 -1
- data/lib/sequel/model/associations.rb +40 -9
- data/lib/sequel/plugins/async_thread_pool.rb +1 -1
- data/lib/sequel/plugins/column_encryption.rb +20 -3
- data/lib/sequel/plugins/concurrent_eager_loading.rb +174 -0
- data/lib/sequel/plugins/pg_array_associations.rb +6 -4
- data/lib/sequel/plugins/rcte_tree.rb +27 -19
- data/lib/sequel/version.rb +1 -1
- metadata +6 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 84b971e8db174387861ce5ccbb36f43ba2ffa39a8b5f88359c1246eac5ff1539
|
4
|
+
data.tar.gz: d90034f07d752fc45f6359d9b38708e865200c1a1f0c9a1ea77c9d24d681e0a3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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 ::
|
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
|
-
# :
|
249
|
-
# :
|
250
|
-
#
|
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
|
-
# :
|
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#
|
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:
|
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
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
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
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
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
|
@@ -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
|
-
|
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
|
-
|
3425
|
-
|
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
|
-
#
|
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
|
-
|
11
|
-
|
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
|
-
|
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
|
-
|
430
|
-
|
431
|
-
associated_pks
|
432
|
-
|
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[:
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
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
|
-
|
211
|
-
|
212
|
-
children
|
213
|
-
|
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[:
|
272
|
-
|
273
|
-
|
274
|
-
|
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
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
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
|
data/lib/sequel/version.rb
CHANGED
@@ -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 =
|
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.
|
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-
|
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.
|
572
|
+
rubygems_version: 3.2.15
|
570
573
|
signing_key:
|
571
574
|
specification_version: 4
|
572
575
|
summary: The Database Toolkit for Ruby
|