sequel 5.72.0 → 5.73.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9e135f19f0f4fb1b78f2c3d0c542ca3db937fa6e1e4960d11120e36a0f6ca314
4
- data.tar.gz: f1550faa63cfda46f2fc2c4142e009e19357ce961f8e63542874cabdefbaf597
3
+ metadata.gz: bf3b8749cd91ac285e66359ae16adf9605c13e673a0cf3f7251527573d71fed6
4
+ data.tar.gz: 817f6980be08b29368eb0f08631d64ae48a4281aa1b2974b589441fea50e2dac
5
5
  SHA512:
6
- metadata.gz: '093c3ca50f23e97bc3a10b1b0b2a4dea3f79e9cc85bff92801676709fad00e36dd8a84156a5179758bf091fe9e1486d65fc3de300a2b8038f181f36d16481918'
7
- data.tar.gz: db0f0c8c81e1fa33b80dbc2e45d477997392965c5aca8a32fc92125843cff4b49045d3d83bd182c9b8bdf980336e1dc97c47a1a32e186e6ef01e5b7d98bbd0a5
6
+ metadata.gz: 6da7c5e7a7c74c4767c55d126adc3743875a5dd0822ee780b22b1ca3c3791cc10d08cf74c984776dbd9fbbcf61d9246bcef4c81e64204e5d72aa59545ef9a2d9
7
+ data.tar.gz: 7ac12f6689d846345fed77e23665679ae4886d60e4d25d4e201a3cfdac4f231882af0aaf3e615bd9981d09c2e9537c1e287fad614f81760f2d897b87402b38b1
data/CHANGELOG CHANGED
@@ -1,3 +1,21 @@
1
+ === 5.73.0 (2023-10-01)
2
+
3
+ * Handle disconnect errors in ibmdb and jdbc/db2 adapters (jeremyevans) (#2083)
4
+
5
+ * Support skipping transactions in Dataset#{import,paged_each} using :skip_transaction option (jeremyevans)
6
+
7
+ * Add Database#transaction :skip_transaction option to skip creating a transaction or savepoint (jeremyevans)
8
+
9
+ * Stop using a transaction for a single query if calling Dataset#import with a dataset (jeremyevans)
10
+
11
+ * Add paged_operations plugin for paged deletes and updates and other custom operations (jeremyevans) (#2080)
12
+
13
+ * Support to_tsquery: :websearch option to Dataset#full_text_search on PostgreSQL 11+ (jeremyevans) (#2075)
14
+
15
+ * Add MassAssignmentRestriction#model and #column for getting the model instance and related column for mass assignment errors (artofhuman, jeremyevans) (#2079)
16
+
17
+ * Stop using base64 library in column_encryption plugin (jeremyevans)
18
+
1
19
  === 5.72.0 (2023-09-01)
2
20
 
3
21
  * Sort caches before marshalling when using schema_caching, index_caching, static_cache_cache, and pg_auto_constraint_validations (jeremyevans)
data/README.rdoc CHANGED
@@ -927,7 +927,7 @@ Sequel fully supports the currently supported versions of Ruby (MRI) and JRuby.
927
927
  support unsupported versions of Ruby or JRuby, but such support may be dropped in any
928
928
  minor version if keeping it becomes a support issue. The minimum Ruby version
929
929
  required to run the current version of Sequel is 1.9.2, and the minimum JRuby version is
930
- 9.0.0.0.
930
+ 9.2.0.0 (due to the bigdecimal dependency).
931
931
 
932
932
  == Maintainer
933
933
 
@@ -0,0 +1,66 @@
1
+ = New Features
2
+
3
+ * A paged_operations plugin has been added, which adds support for
4
+ paged_datasets, paged_update, and paged_delete dataset methods.
5
+ This methods are designed to be used on large datasets, to split
6
+ a large query into separate smaller queries, to avoid locking the
7
+ related database table for a long period of time.
8
+ paged_update and paged_delete operate the same as update and delete,
9
+ returning the number of rows updated or deleted. paged_datasets yields
10
+ one or more datasets representing subsets of the receiver, with the
11
+ union of all of those datasets comprising all records in the receiver:
12
+
13
+ Album.plugin :paged_operations
14
+
15
+ Album.where{name > 'M'}.paged_datasets{|ds| puts ds.sql}
16
+ # Runs: SELECT id FROM albums WHERE (name <= 'M') ORDER BY id LIMIT 1 OFFSET 1000
17
+ # Prints: SELECT * FROM albums WHERE ((name <= 'M') AND ("id" < 1002))
18
+ # Runs: SELECT id FROM albums WHERE ((name <= 'M') AND (id >= 1002)) ORDER BY id LIMIT 1 OFFSET 1000
19
+ # Prints: SELECT * FROM albums WHERE ((name <= 'M') AND ("id" < 2002) AND (id >= 1002))
20
+ # ...
21
+ # Runs: SELECT id FROM albums WHERE ((name <= 'M') AND (id >= 10002)) ORDER BY id LIMIT 1 OFFSET 1000
22
+ # Prints: SELECT * FROM albums WHERE ((name <= 'M') AND (id >= 10002))
23
+
24
+ Album.where{name <= 'M'}.paged_update(:updated_at=>Sequel::CURRENT_TIMESTAMP)
25
+ # SELECT id FROM albums WHERE (name <= 'M') ORDER BY id LIMIT 1 OFFSET 1000
26
+ # UPDATE albums SET updated_at = CURRENT_TIMESTAMP WHERE ((name <= 'M') AND ("id" < 1002))
27
+ # SELECT id FROM albums WHERE ((name <= 'M') AND (id >= 1002)) ORDER BY id LIMIT 1 OFFSET 1000
28
+ # UPDATE albums SET updated_at = CURRENT_TIMESTAMP WHERE ((name <= 'M') AND ("id" < 2002) AND (id >= 1002))
29
+ # ...
30
+ # SELECT id FROM albums WHERE ((name <= 'M') AND (id >= 10002)) ORDER BY id LIMIT 1 OFFSET 1000
31
+ # UPDATE albums SET updated_at = CURRENT_TIMESTAMP WHERE ((name <= 'M') AND (id >= 10002))
32
+
33
+ Album.where{name > 'M'}.paged_delete
34
+ # SELECT id FROM albums WHERE (name > 'M') ORDER BY id LIMIT 1 OFFSET 1000
35
+ # DELETE FROM albums WHERE ((name > 'M') AND (id < 1002))
36
+ # SELECT id FROM albums WHERE (name > 'M') ORDER BY id LIMIT 1 OFFSET 1000
37
+ # DELETE FROM albums WHERE ((name > 'M') AND (id < 2002))
38
+ # ...
39
+ # SELECT id FROM albums WHERE (name > 'M') ORDER BY id LIMIT 1 OFFSET 1000
40
+ # DELETE FROM albums WHERE (name > 'M')
41
+
42
+ * A Dataset#transaction :skip_transaction option is now support to
43
+ checkout a connection from the pool without opening a transaction. This
44
+ makes it easier to handle cases where a transaction may or not be used
45
+ based on configuration/options. Dataset#import and Dataset#paged_each
46
+ now both support the :skip_transaction option to skip transactions.
47
+
48
+ * Dataset#full_text_search now supports the to_tsquery: :websearch option
49
+ on PostgreSQL 11+, to use the websearch_to_tsquery database function.
50
+
51
+ * The Sequel::MassAssignmentRestriction exception now supports model
52
+ and column methods to get provide additional information about the
53
+ exception. Additionally, the exception message now includes information
54
+ about the model class.
55
+
56
+ = Other Improvements
57
+
58
+ * The ibmdb and jdbc/db2 adapter now both handle disconnect errors
59
+ correctly, removing the related connection from the pool.
60
+
61
+ * Dataset#import no longer uses an explicit transaction if given a dataset
62
+ value, as in that case, only a single query is used.
63
+
64
+ * The column_encryption plugin no longer uses the base64 library. The
65
+ base64 library is moving from the standard library to a bundled gem
66
+ in Ruby 3.4, and this avoids having a dependency on it.
@@ -301,7 +301,7 @@ module Sequel
301
301
  end
302
302
 
303
303
  def database_exception_sqlstate(exception, opts)
304
- exception.sqlstate
304
+ exception.sqlstate if exception.respond_to?(:sqlstate)
305
305
  end
306
306
 
307
307
  def dataset_class_default
@@ -36,6 +36,10 @@ module Sequel
36
36
 
37
37
  private
38
38
 
39
+ def database_exception_sqlstate(exception, opts)
40
+ exception.sql_state if exception.respond_to?(:sql_state)
41
+ end
42
+
39
43
  def set_ps_arg(cps, arg, i)
40
44
  case arg
41
45
  when Sequel::SQL::Blob
@@ -215,6 +215,18 @@ module Sequel
215
215
  DATABASE_ERROR_REGEXPS
216
216
  end
217
217
 
218
+ DISCONNECT_SQL_STATES = %w'40003 08001 08003'.freeze
219
+ def disconnect_error?(exception, opts)
220
+ sqlstate = database_exception_sqlstate(exception, opts)
221
+
222
+ case sqlstate
223
+ when *DISCONNECT_SQL_STATES
224
+ true
225
+ else
226
+ super
227
+ end
228
+ end
229
+
218
230
  # DB2 has issues with quoted identifiers, so
219
231
  # turn off database quoting by default.
220
232
  def quote_identifiers_default
@@ -1798,7 +1798,7 @@ module Sequel
1798
1798
  # :phrase :: Similar to :plain, but also adding an ILIKE filter to ensure that
1799
1799
  # returned rows also include the exact phrase used.
1800
1800
  # :rank :: Set to true to order by the rank, so that closer matches are returned first.
1801
- # :to_tsquery :: Can be set to :plain or :phrase to specify the function to use to
1801
+ # :to_tsquery :: Can be set to :plain, :phrase, or :websearch to specify the function to use to
1802
1802
  # convert the terms to a ts_query.
1803
1803
  # :tsquery :: Specifies the terms argument is already a valid SQL expression returning a
1804
1804
  # tsquery, and can be used directly in the query.
@@ -1818,6 +1818,8 @@ module Sequel
1818
1818
  query_func = case to_tsquery = opts[:to_tsquery]
1819
1819
  when :phrase, :plain
1820
1820
  :"#{to_tsquery}to_tsquery"
1821
+ when :websearch
1822
+ :"websearch_to_tsquery"
1821
1823
  else
1822
1824
  (opts[:phrase] || opts[:plain]) ? :plainto_tsquery : :to_tsquery
1823
1825
  end
@@ -712,8 +712,9 @@ module Sequel
712
712
  e = options[:ignore_index_errors] || options[:if_not_exists]
713
713
  generator.indexes.each do |index|
714
714
  begin
715
- pr = proc{index_sql_list(name, [index]).each{|sql| execute_ddl(sql)}}
716
- supports_transactional_ddl? ? transaction(:savepoint=>:only, &pr) : pr.call
715
+ transaction(:savepoint=>:only, :skip_transaction=>supports_transactional_ddl? == false) do
716
+ index_sql_list(name, [index]).each{|sql| execute_ddl(sql)}
717
+ end
717
718
  rescue Error
718
719
  raise unless e
719
720
  end
@@ -166,6 +166,8 @@ module Sequel
166
166
  # uses :auto_savepoint, you can set this to false to not use a savepoint.
167
167
  # If the value given for this option is :only, it will only create a
168
168
  # savepoint if it is inside a transaction.
169
+ # :skip_transaction :: If set, do not actually open a transaction or savepoint,
170
+ # just checkout a connection and yield it.
169
171
  #
170
172
  # PostgreSQL specific options:
171
173
  #
@@ -193,6 +195,10 @@ module Sequel
193
195
  end
194
196
  else
195
197
  synchronize(opts[:server]) do |conn|
198
+ if opts[:skip_transaction]
199
+ return yield(conn)
200
+ end
201
+
196
202
  if opts[:savepoint] == :only
197
203
  if supports_savepoints?
198
204
  if _trans(conn)
@@ -356,9 +356,11 @@ module Sequel
356
356
  # This does not have an effect if +values+ is a Dataset.
357
357
  # :server :: Set the server/shard to use for the transaction and insert
358
358
  # queries.
359
+ # :skip_transaction :: Do not use a transaction even when using multiple
360
+ # INSERT queries.
359
361
  # :slice :: Same as :commit_every, :commit_every takes precedence.
360
362
  def import(columns, values, opts=OPTS)
361
- return @db.transaction{insert(columns, values)} if values.is_a?(Dataset)
363
+ return insert(columns, values) if values.is_a?(Dataset)
362
364
 
363
365
  return if values.empty?
364
366
  raise(Error, 'Using Sequel::Dataset#import with an empty column array is not allowed') if columns.empty?
@@ -588,6 +590,8 @@ module Sequel
588
590
  # if your ORDER BY expressions are not simple columns, if they contain
589
591
  # qualified identifiers that would be ambiguous unqualified, if they contain
590
592
  # any identifiers that are aliased in SELECT, and potentially other cases.
593
+ # :skip_transaction :: Do not use a transaction. This can be useful if you want to prevent
594
+ # a lock on the database table, at the expense of consistency.
591
595
  #
592
596
  # Examples:
593
597
  #
@@ -1111,11 +1115,9 @@ module Sequel
1111
1115
  # are provided. When only a single value or statement is provided, then yield
1112
1116
  # without using a transaction.
1113
1117
  def _import_transaction(values, trans_opts, &block)
1114
- if values.length > 1
1115
- @db.transaction(trans_opts, &block)
1116
- else
1117
- yield
1118
- end
1118
+ # OK to mutate trans_opts as it is generated by _import
1119
+ trans_opts[:skip_transaction] = true if values.length <= 1
1120
+ @db.transaction(trans_opts, &block)
1119
1121
  end
1120
1122
 
1121
1123
  # Internals of +select_hash+ and +select_hash_groups+
@@ -482,11 +482,7 @@ module Sequel
482
482
  @use_transactions
483
483
  end
484
484
 
485
- if use_trans
486
- db.transaction(&block)
487
- else
488
- yield
489
- end
485
+ db.transaction(:skip_transaction=>use_trans == false, &block)
490
486
  end
491
487
 
492
488
  # Load the migration file, raising an exception if the file does not define
@@ -1945,8 +1945,10 @@ module Sequel
1945
1945
  end
1946
1946
 
1947
1947
  # If transactions should be used, wrap the yield in a transaction block.
1948
- def checked_transaction(opts=OPTS)
1949
- use_transaction?(opts) ? db.transaction({:server=>this_server}.merge!(opts)){yield} : yield
1948
+ def checked_transaction(opts=OPTS, &block)
1949
+ h = {:server=>this_server}.merge!(opts)
1950
+ h[:skip_transaction] = true unless use_transaction?(opts)
1951
+ db.transaction(h, &block)
1950
1952
  end
1951
1953
 
1952
1954
  # Change the value of the column to given value, recording the change.
@@ -2031,19 +2033,20 @@ module Sequel
2031
2033
  meths = setter_methods(type)
2032
2034
  strict = strict_param_setting
2033
2035
  hash.each do |k,v|
2036
+ k = k.to_s
2034
2037
  m = "#{k}="
2035
2038
  if meths.include?(m)
2036
2039
  set_column_value(m, v)
2037
2040
  elsif strict
2038
2041
  # Avoid using respond_to? or creating symbols from user input
2039
2042
  if public_methods.map(&:to_s).include?(m)
2040
- if Array(model.primary_key).map(&:to_s).member?(k.to_s) && model.restrict_primary_key?
2041
- raise MassAssignmentRestriction, "#{k} is a restricted primary key"
2043
+ if Array(model.primary_key).map(&:to_s).member?(k) && model.restrict_primary_key?
2044
+ raise MassAssignmentRestriction.create("#{k} is a restricted primary key", self, k)
2042
2045
  else
2043
- raise MassAssignmentRestriction, "#{k} is a restricted column"
2046
+ raise MassAssignmentRestriction.create("#{k} is a restricted column", self, k)
2044
2047
  end
2045
2048
  else
2046
- raise MassAssignmentRestriction, "method #{m} doesn't exist"
2049
+ raise MassAssignmentRestriction.create("method #{m} doesn't exist", self, k)
2047
2050
  end
2048
2051
  end
2049
2052
  end
@@ -2147,8 +2150,9 @@ module Sequel
2147
2150
  # # DELETE FROM artists WHERE (id = 2)
2148
2151
  # # ...
2149
2152
  def destroy
2150
- pr = proc{all(&:destroy).length}
2151
- model.use_transactions ? @db.transaction(:server=>opts[:server], &pr) : pr.call
2153
+ @db.transaction(:server=>opts[:server], :skip_transaction=>model.use_transactions == false) do
2154
+ all(&:destroy).length
2155
+ end
2152
2156
  end
2153
2157
 
2154
2158
  # If there is no order already defined on this dataset, order it by
@@ -2228,11 +2232,17 @@ module Sequel
2228
2232
 
2229
2233
  private
2230
2234
 
2235
+ # Return the dataset ordered by the model's primary key. This should not
2236
+ # be used if the model does not have a primary key.
2237
+ def _force_primary_key_order
2238
+ cached_dataset(:_pk_order_ds){order(*model.primary_key)}
2239
+ end
2240
+
2231
2241
  # If the dataset is not already ordered, and the model has a primary key,
2232
2242
  # return a clone ordered by the primary key.
2233
2243
  def _primary_key_order
2234
- if @opts[:order].nil? && model && (pk = model.primary_key)
2235
- cached_dataset(:_pk_order_ds){order(*pk)}
2244
+ if @opts[:order].nil? && model && model.primary_key
2245
+ _force_primary_key_order
2236
2246
  end
2237
2247
  end
2238
2248
 
@@ -24,11 +24,23 @@ module Sequel
24
24
  UndefinedAssociation = Class.new(Error)
25
25
  ).name
26
26
 
27
- (
28
27
  # Raised when a mass assignment method is called in strict mode with either a restricted column
29
28
  # or a column without a setter method.
30
- MassAssignmentRestriction = Class.new(Error)
31
- ).name
29
+ class MassAssignmentRestriction < Error
30
+ # The Sequel::Model object related to this exception.
31
+ attr_reader :model
32
+
33
+ # The column related to this exception, as a string.
34
+ attr_reader :column
35
+
36
+ # Create an instance of this class with the model and column set.
37
+ def self.create(msg, model, column)
38
+ e = new("#{msg} for class #{model.class.inspect}")
39
+ e.instance_variable_set(:@model, model)
40
+ e.instance_variable_set(:@column, column)
41
+ e
42
+ end
43
+ end
32
44
 
33
45
  # Exception class raised when +raise_on_save_failure+ is set and validation fails
34
46
  class ValidationFailed < Error
@@ -31,7 +31,6 @@ rescue RuntimeError, OpenSSL::Cipher::CipherError
31
31
  # :nocov:
32
32
  end
33
33
 
34
- require 'base64'
35
34
  require 'securerandom'
36
35
 
37
36
  module Sequel
@@ -375,7 +374,7 @@ module Sequel
375
374
  # Decrypt using any supported format and any available key.
376
375
  def decrypt(data)
377
376
  begin
378
- data = Base64.urlsafe_decode64(data)
377
+ data = urlsafe_decode64(data)
379
378
  rescue ArgumentError
380
379
  raise Error, "Unable to decode encrypted column: invalid base64"
381
380
  end
@@ -448,7 +447,7 @@ module Sequel
448
447
  # The prefix string of columns for the given search type and the first configured encryption key.
449
448
  # Used to find values that do not use this prefix in order to perform reencryption.
450
449
  def current_key_prefix(search_type)
451
- Base64.urlsafe_encode64("#{search_type.chr}\0#{@key_id.chr}")
450
+ urlsafe_encode64("#{search_type.chr}\0#{@key_id.chr}")
452
451
  end
453
452
 
454
453
  # The prefix values to search for the given data (an array of strings), assuming the column uses
@@ -472,11 +471,33 @@ module Sequel
472
471
 
473
472
  private
474
473
 
474
+ if RUBY_VERSION >= '2.4'
475
+ def decode64(str)
476
+ str.unpack1("m0")
477
+ end
478
+ # :nocov:
479
+ else
480
+ def decode64(str)
481
+ str.unpack("m0")[0]
482
+ end
483
+ # :nocov:
484
+ end
485
+
486
+ def urlsafe_encode64(bin)
487
+ str = [bin].pack("m0")
488
+ str.tr!("+/", "-_")
489
+ str
490
+ end
491
+
492
+ def urlsafe_decode64(str)
493
+ decode64(str.tr("-_", "+/"))
494
+ end
495
+
475
496
  # An array of strings, one for each configured encryption key, to find encypted values matching
476
497
  # the given data and search format.
477
498
  def _search_prefixes(data, search_type)
478
499
  @key_map.map do |key_id, (key, _)|
479
- Base64.urlsafe_encode64(_search_prefix(data, search_type, key_id, key))
500
+ urlsafe_encode64(_search_prefix(data, search_type, key_id, key))
480
501
  end
481
502
  end
482
503
 
@@ -509,7 +530,7 @@ module Sequel
509
530
  cipher_text << cipher.update(data) if data_size > 0
510
531
  cipher_text << cipher.final
511
532
 
512
- Base64.urlsafe_encode64("#{prefix}#{random_data}#{cipher_iv}#{cipher.auth_tag}#{cipher_text}")
533
+ urlsafe_encode64("#{prefix}#{random_data}#{cipher_iv}#{cipher.auth_tag}#{cipher_text}")
513
534
  end
514
535
  end
515
536
 
@@ -0,0 +1,181 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Sequel
4
+ module Plugins
5
+ # The paged_operations plugin adds +paged_update+ and
6
+ # +paged_delete+ dataset methods. These behave similarly to
7
+ # the default +update+ and +delete+ dataset methods, except
8
+ # that the update or deletion is done in potentially multiple
9
+ # queries (by default, affecting 1000 rows per query).
10
+ # For a large table, this prevents the change from
11
+ # locking the table for a long period of time.
12
+ #
13
+ # Because the point of this is to prevent locking tables for
14
+ # long periods of time, the separate queries are not contained
15
+ # in a transaction, which means if a later query fails,
16
+ # earlier queries will still be committed. You could prevent
17
+ # this by using a transaction manually, but that defeats the
18
+ # purpose of using these methods.
19
+ #
20
+ # Examples:
21
+ #
22
+ # Album.where{name <= 'M'}.paged_update(updated_at: Sequel::CURRENT_TIMESTAMP)
23
+ # # SELECT id FROM albums WHERE (name <= 'M') ORDER BY id LIMIT 1 OFFSET 1000
24
+ # # UPDATE albums SET updated_at = CURRENT_TIMESTAMP WHERE ((name <= 'M') AND ("id" < 1002))
25
+ # # SELECT id FROM albums WHERE ((name <= 'M') AND (id >= 1002)) ORDER BY id LIMIT 1 OFFSET 1000
26
+ # # UPDATE albums SET updated_at = CURRENT_TIMESTAMP WHERE ((name <= 'M') AND ("id" < 2002) AND (id >= 1002))
27
+ # # ...
28
+ # # SELECT id FROM albums WHERE ((name <= 'M') AND (id >= 10002)) ORDER BY id LIMIT 1 OFFSET 1000
29
+ # # UPDATE albums SET updated_at = CURRENT_TIMESTAMP WHERE ((name <= 'M') AND (id >= 10002))
30
+ #
31
+ # Album.where{name > 'M'}.paged_delete
32
+ # # SELECT id FROM albums WHERE (name > 'M') ORDER BY id LIMIT 1 OFFSET 1000
33
+ # # DELETE FROM albums WHERE ((name > 'M') AND (id < 1002))
34
+ # # SELECT id FROM albums WHERE (name > 'M') ORDER BY id LIMIT 1 OFFSET 1000
35
+ # # DELETE FROM albums WHERE ((name > 'M') AND (id < 2002))
36
+ # # ...
37
+ # # SELECT id FROM albums WHERE (name > 'M') ORDER BY id LIMIT 1 OFFSET 1000
38
+ # # DELETE FROM albums WHERE (name > 'M')
39
+ #
40
+ # The plugin also adds a +paged_datasets+ method that will yield
41
+ # separate datasets limited in size that in total handle all
42
+ # rows in the receiver:
43
+ #
44
+ # Album.where{name > 'M'}.paged_datasets{|ds| puts ds.sql}
45
+ # # Runs: SELECT id FROM albums WHERE (name <= 'M') ORDER BY id LIMIT 1 OFFSET 1000
46
+ # # Prints: SELECT * FROM albums WHERE ((name <= 'M') AND ("id" < 1002))
47
+ # # Runs: SELECT id FROM albums WHERE ((name <= 'M') AND (id >= 1002)) ORDER BY id LIMIT 1 OFFSET 1000
48
+ # # Prints: SELECT * FROM albums WHERE ((name <= 'M') AND ("id" < 2002) AND (id >= 1002))
49
+ # # ...
50
+ # # Runs: SELECT id FROM albums WHERE ((name <= 'M') AND (id >= 10002)) ORDER BY id LIMIT 1 OFFSET 1000
51
+ # # Prints: SELECT * FROM albums WHERE ((name <= 'M') AND (id >= 10002))
52
+ #
53
+ # To set the number of rows per page, pass a :rows_per_page option:
54
+ #
55
+ # Album.where{name <= 'M'}.paged_update({x: Sequel[:x] + 1}, rows_per_page: 4)
56
+ # # SELECT id FROM albums WHERE (name <= 'M') ORDER BY id LIMIT 1 OFFSET 4
57
+ # # UPDATE albums SET x = x + 1 WHERE ((name <= 'M') AND ("id" < 5))
58
+ # # SELECT id FROM albums WHERE ((name <= 'M') AND (id >= 5)) ORDER BY id LIMIT 1 OFFSET 4
59
+ # # UPDATE albums SET x = x + 1 WHERE ((name <= 'M') AND ("id" < 9) AND (id >= 5))
60
+ # # ...
61
+ # # SELECT id FROM albums WHERE ((name <= 'M') AND (id >= 12345)) ORDER BY id LIMIT 1 OFFSET 4
62
+ # # UPDATE albums SET x = x + 1 WHERE ((name <= 'M') AND (id >= 12345))
63
+ #
64
+ # You should avoid using +paged_update+ or +paged_datasets+
65
+ # with updates that modify the primary key, as such usage is
66
+ # not supported by this plugin.
67
+ #
68
+ # This plugin only supports models with scalar primary keys.
69
+ #
70
+ # Usage:
71
+ #
72
+ # # Make all model subclasses support paged update/delete/datasets
73
+ # # (called before loading subclasses)
74
+ # Sequel::Model.plugin :paged_operations
75
+ #
76
+ # # Make the Album class support paged update/delete/datasets
77
+ # Album.plugin :paged_operations
78
+ module PagedOperations
79
+ module ClassMethods
80
+ Plugins.def_dataset_methods(self, [:paged_datasets, :paged_delete, :paged_update])
81
+ end
82
+
83
+ module DatasetMethods
84
+ # Yield datasets for subsets of the receiver that are limited
85
+ # to no more than 1000 rows (you can configure the number of
86
+ # rows using +:rows_per_page+).
87
+ #
88
+ # Options:
89
+ # :rows_per_page :: The maximum number of rows in each yielded dataset
90
+ # (unless concurrent modifications are made to the table).
91
+ def paged_datasets(opts=OPTS)
92
+ unless defined?(yield)
93
+ return enum_for(:paged_datasets, opts)
94
+ end
95
+
96
+ pk = _paged_operations_pk(:paged_update)
97
+ base_offset_ds = offset_ds = _paged_operations_offset_ds(opts)
98
+ first = nil
99
+
100
+ while last = offset_ds.get(pk)
101
+ ds = where(pk < last)
102
+ ds = ds.where(pk >= first) if first
103
+ yield ds
104
+ first = last
105
+ offset_ds = base_offset_ds.where(pk >= first)
106
+ end
107
+
108
+ ds = self
109
+ ds = ds.where(pk >= first) if first
110
+ yield ds
111
+ nil
112
+ end
113
+
114
+ # Delete all rows of the dataset using using multiple queries so that
115
+ # no more than 1000 rows are deleted at a time (you can configure the
116
+ # number of rows using +:rows_per_page+).
117
+ #
118
+ # Options:
119
+ # :rows_per_page :: The maximum number of rows affected by each DELETE query
120
+ # (unless concurrent modifications are made to the table).
121
+ def paged_delete(opts=OPTS)
122
+ if (db.database_type == :oracle && !supports_fetch_next_rows?) || (db.database_type == :mssql && !is_2012_or_later?)
123
+ raise Error, "paged_delete is not supported on MSSQL/Oracle when using emulated offsets"
124
+ end
125
+ pk = _paged_operations_pk(:paged_delete)
126
+ rows_deleted = 0
127
+ offset_ds = _paged_operations_offset_ds(opts)
128
+ while last = offset_ds.get(pk)
129
+ rows_deleted += where(pk < last).delete
130
+ end
131
+ rows_deleted + delete
132
+ end
133
+
134
+ # Update all rows of the dataset using using multiple queries so that
135
+ # no more than 1000 rows are updated at a time (you can configure the
136
+ # number of rows using +:rows_per_page+). All arguments are
137
+ # passed to Dataset#update.
138
+ #
139
+ # Options:
140
+ # :rows_per_page :: The maximum number of rows affected by each UPDATE query
141
+ # (unless concurrent modifications are made to the table).
142
+ def paged_update(values, opts=OPTS)
143
+ rows_updated = 0
144
+ paged_datasets(opts) do |ds|
145
+ rows_updated += ds.update(values)
146
+ end
147
+ rows_updated
148
+ end
149
+
150
+ private
151
+
152
+ # Run some basic checks common to paged_{datasets,delete,update}
153
+ # and return the primary key to operate on as a Sequel::Identifier.
154
+ def _paged_operations_pk(meth)
155
+ raise Error, "cannot use #{meth} if dataset has a limit or offset" if @opts[:limit] || @opts[:offset]
156
+ if db.database_type == :db2 && db.offset_strategy == :emulate
157
+ raise Error, "the paged_operations plugin is not supported on DB2 when using emulated offsets, set the :offset_strategy Database option to 'limit_offset' or 'offset_fetch'"
158
+ end
159
+
160
+ case pk = model.primary_key
161
+ when Symbol
162
+ Sequel.identifier(pk)
163
+ when Array
164
+ raise Error, "cannot use #{meth} on a model with a composite primary key"
165
+ else
166
+ raise Error, "cannot use #{meth} on a model without a primary key"
167
+ end
168
+ end
169
+
170
+ # The dataset that will be used by paged_{datasets,delete,update}
171
+ # to get the upper limit for the next query.
172
+ def _paged_operations_offset_ds(opts)
173
+ if rows_per_page = opts[:rows_per_page]
174
+ raise Error, ":rows_per_page option must be at least 1" unless rows_per_page >= 1
175
+ end
176
+ _force_primary_key_order.offset(rows_per_page || 1000)
177
+ end
178
+ end
179
+ end
180
+ end
181
+ 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 = 72
9
+ MINOR = 73
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.72.0
4
+ version: 5.73.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: 2023-09-01 00:00:00.000000000 Z
11
+ date: 2023-10-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bigdecimal
@@ -219,6 +219,7 @@ extra_rdoc_files:
219
219
  - doc/release_notes/5.70.0.txt
220
220
  - doc/release_notes/5.71.0.txt
221
221
  - doc/release_notes/5.72.0.txt
222
+ - doc/release_notes/5.73.0.txt
222
223
  - doc/release_notes/5.8.0.txt
223
224
  - doc/release_notes/5.9.0.txt
224
225
  files:
@@ -319,6 +320,7 @@ files:
319
320
  - doc/release_notes/5.70.0.txt
320
321
  - doc/release_notes/5.71.0.txt
321
322
  - doc/release_notes/5.72.0.txt
323
+ - doc/release_notes/5.73.0.txt
322
324
  - doc/release_notes/5.8.0.txt
323
325
  - doc/release_notes/5.9.0.txt
324
326
  - doc/schema_modification.rdoc
@@ -572,6 +574,7 @@ files:
572
574
  - lib/sequel/plugins/nested_attributes.rb
573
575
  - lib/sequel/plugins/optimistic_locking.rb
574
576
  - lib/sequel/plugins/optimistic_locking_base.rb
577
+ - lib/sequel/plugins/paged_operations.rb
575
578
  - lib/sequel/plugins/pg_array_associations.rb
576
579
  - lib/sequel/plugins/pg_auto_constraint_validations.rb
577
580
  - lib/sequel/plugins/pg_row.rb