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 +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
|