sequel 5.60.1 → 5.81.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 +280 -0
- data/MIT-LICENSE +1 -1
- data/README.rdoc +27 -24
- data/doc/advanced_associations.rdoc +16 -14
- data/doc/association_basics.rdoc +53 -17
- data/doc/cheat_sheet.rdoc +3 -3
- data/doc/code_order.rdoc +5 -3
- data/doc/dataset_basics.rdoc +1 -1
- data/doc/mass_assignment.rdoc +1 -1
- data/doc/migration.rdoc +15 -0
- data/doc/model_hooks.rdoc +1 -1
- data/doc/object_model.rdoc +8 -8
- data/doc/opening_databases.rdoc +18 -7
- data/doc/postgresql.rdoc +8 -8
- data/doc/querying.rdoc +7 -2
- data/doc/release_notes/5.61.0.txt +43 -0
- data/doc/release_notes/5.62.0.txt +132 -0
- data/doc/release_notes/5.63.0.txt +33 -0
- data/doc/release_notes/5.64.0.txt +50 -0
- data/doc/release_notes/5.65.0.txt +21 -0
- data/doc/release_notes/5.66.0.txt +24 -0
- data/doc/release_notes/5.67.0.txt +32 -0
- data/doc/release_notes/5.68.0.txt +61 -0
- data/doc/release_notes/5.69.0.txt +26 -0
- data/doc/release_notes/5.70.0.txt +35 -0
- data/doc/release_notes/5.71.0.txt +21 -0
- data/doc/release_notes/5.72.0.txt +33 -0
- data/doc/release_notes/5.73.0.txt +66 -0
- data/doc/release_notes/5.74.0.txt +45 -0
- data/doc/release_notes/5.75.0.txt +35 -0
- data/doc/release_notes/5.76.0.txt +86 -0
- data/doc/release_notes/5.77.0.txt +63 -0
- data/doc/release_notes/5.78.0.txt +67 -0
- data/doc/release_notes/5.79.0.txt +28 -0
- data/doc/release_notes/5.80.0.txt +40 -0
- data/doc/release_notes/5.81.0.txt +31 -0
- data/doc/schema_modification.rdoc +3 -3
- data/doc/security.rdoc +9 -9
- data/doc/sharding.rdoc +3 -1
- data/doc/sql.rdoc +14 -14
- data/doc/testing.rdoc +16 -12
- data/doc/transactions.rdoc +6 -6
- data/doc/virtual_rows.rdoc +1 -1
- data/lib/sequel/adapters/ibmdb.rb +1 -1
- data/lib/sequel/adapters/jdbc/h2.rb +3 -0
- data/lib/sequel/adapters/jdbc/hsqldb.rb +2 -0
- data/lib/sequel/adapters/jdbc/postgresql.rb +3 -0
- data/lib/sequel/adapters/jdbc/sqlanywhere.rb +15 -0
- data/lib/sequel/adapters/jdbc/sqlserver.rb +4 -0
- data/lib/sequel/adapters/jdbc.rb +10 -6
- data/lib/sequel/adapters/mysql.rb +19 -7
- data/lib/sequel/adapters/mysql2.rb +2 -2
- data/lib/sequel/adapters/odbc/mssql.rb +1 -1
- data/lib/sequel/adapters/oracle.rb +1 -0
- data/lib/sequel/adapters/postgres.rb +10 -5
- data/lib/sequel/adapters/shared/access.rb +9 -1
- data/lib/sequel/adapters/shared/db2.rb +12 -0
- data/lib/sequel/adapters/shared/mssql.rb +71 -9
- data/lib/sequel/adapters/shared/mysql.rb +82 -3
- data/lib/sequel/adapters/shared/oracle.rb +17 -7
- data/lib/sequel/adapters/shared/postgres.rb +469 -160
- data/lib/sequel/adapters/shared/sqlanywhere.rb +18 -5
- data/lib/sequel/adapters/shared/sqlite.rb +39 -3
- data/lib/sequel/adapters/sqlite.rb +42 -3
- data/lib/sequel/adapters/trilogy.rb +117 -0
- data/lib/sequel/connection_pool/sharded_threaded.rb +16 -11
- data/lib/sequel/connection_pool/sharded_timed_queue.rb +374 -0
- data/lib/sequel/connection_pool/threaded.rb +14 -8
- data/lib/sequel/connection_pool/timed_queue.rb +270 -0
- data/lib/sequel/connection_pool.rb +57 -31
- data/lib/sequel/database/connecting.rb +25 -1
- data/lib/sequel/database/dataset.rb +16 -6
- data/lib/sequel/database/misc.rb +65 -14
- data/lib/sequel/database/query.rb +72 -1
- data/lib/sequel/database/schema_generator.rb +1 -1
- data/lib/sequel/database/schema_methods.rb +12 -5
- data/lib/sequel/database/transactions.rb +6 -0
- data/lib/sequel/dataset/actions.rb +60 -13
- data/lib/sequel/dataset/dataset_module.rb +1 -1
- data/lib/sequel/dataset/deprecated_singleton_class_methods.rb +42 -0
- data/lib/sequel/dataset/features.rb +15 -1
- data/lib/sequel/dataset/graph.rb +1 -0
- data/lib/sequel/dataset/misc.rb +12 -2
- data/lib/sequel/dataset/placeholder_literalizer.rb +20 -9
- data/lib/sequel/dataset/query.rb +119 -45
- data/lib/sequel/dataset/sql.rb +58 -36
- data/lib/sequel/dataset.rb +4 -0
- data/lib/sequel/exceptions.rb +5 -0
- data/lib/sequel/extensions/_model_pg_row.rb +0 -12
- data/lib/sequel/extensions/_pretty_table.rb +1 -1
- data/lib/sequel/extensions/any_not_empty.rb +2 -2
- data/lib/sequel/extensions/async_thread_pool.rb +21 -13
- data/lib/sequel/extensions/auto_cast_date_and_time.rb +94 -0
- data/lib/sequel/extensions/auto_literal_strings.rb +1 -1
- data/lib/sequel/extensions/caller_logging.rb +4 -1
- data/lib/sequel/extensions/connection_expiration.rb +15 -9
- data/lib/sequel/extensions/connection_validator.rb +16 -11
- data/lib/sequel/extensions/constraint_validations.rb +1 -1
- data/lib/sequel/extensions/date_arithmetic.rb +1 -1
- data/lib/sequel/extensions/duplicate_columns_handler.rb +10 -9
- data/lib/sequel/extensions/index_caching.rb +5 -1
- data/lib/sequel/extensions/looser_typecasting.rb +3 -0
- data/lib/sequel/extensions/migration.rb +65 -15
- data/lib/sequel/extensions/named_timezones.rb +22 -6
- data/lib/sequel/extensions/pg_array.rb +33 -4
- data/lib/sequel/extensions/pg_auto_parameterize.rb +509 -0
- data/lib/sequel/extensions/pg_auto_parameterize_in_array.rb +110 -0
- data/lib/sequel/extensions/pg_enum.rb +1 -2
- data/lib/sequel/extensions/pg_extended_date_support.rb +38 -27
- data/lib/sequel/extensions/pg_extended_integer_support.rb +116 -0
- data/lib/sequel/extensions/pg_hstore.rb +5 -0
- data/lib/sequel/extensions/pg_inet.rb +10 -11
- data/lib/sequel/extensions/pg_interval.rb +10 -11
- data/lib/sequel/extensions/pg_json.rb +10 -10
- data/lib/sequel/extensions/pg_json_ops.rb +3 -3
- data/lib/sequel/extensions/pg_multirange.rb +6 -11
- data/lib/sequel/extensions/pg_range.rb +9 -14
- data/lib/sequel/extensions/pg_row.rb +20 -19
- data/lib/sequel/extensions/pg_timestamptz.rb +27 -3
- data/lib/sequel/extensions/provenance.rb +110 -0
- data/lib/sequel/extensions/round_timestamps.rb +1 -1
- data/lib/sequel/extensions/schema_caching.rb +1 -1
- data/lib/sequel/extensions/schema_dumper.rb +32 -9
- data/lib/sequel/extensions/server_block.rb +2 -1
- data/lib/sequel/extensions/set_literalizer.rb +58 -0
- data/lib/sequel/extensions/sqlite_json_ops.rb +76 -18
- data/lib/sequel/extensions/temporarily_release_connection.rb +178 -0
- data/lib/sequel/extensions/transaction_connection_validator.rb +78 -0
- data/lib/sequel/model/associations.rb +38 -11
- data/lib/sequel/model/base.rb +32 -18
- data/lib/sequel/model/dataset_module.rb +3 -0
- data/lib/sequel/model/exceptions.rb +15 -3
- data/lib/sequel/plugins/auto_validations.rb +53 -15
- data/lib/sequel/plugins/class_table_inheritance.rb +2 -2
- data/lib/sequel/plugins/column_encryption.rb +28 -7
- data/lib/sequel/plugins/composition.rb +2 -2
- data/lib/sequel/plugins/concurrent_eager_loading.rb +4 -4
- data/lib/sequel/plugins/constraint_validations.rb +8 -5
- data/lib/sequel/plugins/defaults_setter.rb +16 -0
- data/lib/sequel/plugins/dirty.rb +1 -1
- data/lib/sequel/plugins/finder.rb +4 -2
- data/lib/sequel/plugins/input_transformer.rb +1 -1
- data/lib/sequel/plugins/list.rb +5 -2
- data/lib/sequel/plugins/many_through_many.rb +1 -1
- data/lib/sequel/plugins/mssql_optimistic_locking.rb +8 -38
- data/lib/sequel/plugins/nested_attributes.rb +4 -4
- data/lib/sequel/plugins/optimistic_locking.rb +9 -42
- data/lib/sequel/plugins/optimistic_locking_base.rb +55 -0
- data/lib/sequel/plugins/paged_operations.rb +181 -0
- data/lib/sequel/plugins/pg_auto_constraint_validations.rb +9 -3
- data/lib/sequel/plugins/pg_xmin_optimistic_locking.rb +109 -0
- data/lib/sequel/plugins/prepared_statements.rb +2 -1
- data/lib/sequel/plugins/prepared_statements_safe.rb +2 -1
- data/lib/sequel/plugins/primary_key_lookup_check_values.rb +154 -0
- data/lib/sequel/plugins/rcte_tree.rb +7 -4
- data/lib/sequel/plugins/single_table_inheritance.rb +8 -0
- data/lib/sequel/plugins/sql_comments.rb +1 -1
- data/lib/sequel/plugins/static_cache.rb +38 -0
- data/lib/sequel/plugins/static_cache_cache.rb +5 -1
- data/lib/sequel/plugins/tactical_eager_loading.rb +14 -14
- data/lib/sequel/plugins/validate_associated.rb +22 -12
- data/lib/sequel/plugins/validation_helpers.rb +29 -2
- data/lib/sequel/plugins/validation_helpers_generic_type_messages.rb +73 -0
- data/lib/sequel/version.rb +2 -2
- metadata +79 -6
|
@@ -50,10 +50,10 @@ module Sequel
|
|
|
50
50
|
# support concurrent eager loading. Taking this example from the
|
|
51
51
|
# {Advanced Associations guide}[rdoc-ref:doc/advanced_associations.rdoc]
|
|
52
52
|
#
|
|
53
|
-
# Album.many_to_one :artist, :
|
|
53
|
+
# Album.many_to_one :artist, eager_loader: (proc do |eo_opts|
|
|
54
54
|
# eo_opts[:rows].each{|album| album.associations[:artist] = nil}
|
|
55
55
|
# id_map = eo_opts[:id_map]
|
|
56
|
-
# Artist.where(:
|
|
56
|
+
# Artist.where(id: id_map.keys).all do |artist|
|
|
57
57
|
# if albums = id_map[artist.id]
|
|
58
58
|
# albums.each do |album|
|
|
59
59
|
# album.associations[:artist] = artist
|
|
@@ -74,12 +74,12 @@ module Sequel
|
|
|
74
74
|
# the code that loads the objects, since that will prevent concurrent loading.
|
|
75
75
|
# So after the changes, the custom eager loader would look like this:
|
|
76
76
|
#
|
|
77
|
-
# Album.many_to_one :artist, :
|
|
77
|
+
# Album.many_to_one :artist, eager_loader: (proc do |eo_opts|
|
|
78
78
|
# Sequel.synchronize_with(eo[:mutex]) do
|
|
79
79
|
# eo_opts[:rows].each{|album| album.associations[:artist] = nil}
|
|
80
80
|
# end
|
|
81
81
|
# id_map = eo_opts[:id_map]
|
|
82
|
-
# rows = Artist.where(:
|
|
82
|
+
# rows = Artist.where(id: id_map.keys).all
|
|
83
83
|
# Sequel.synchronize_with(eo[:mutex]) do
|
|
84
84
|
# rows.each do |artist|
|
|
85
85
|
# if albums = id_map[artist.id]
|
|
@@ -125,14 +125,15 @@ module Sequel
|
|
|
125
125
|
ds = @dataset.with_quote_identifiers(false)
|
|
126
126
|
table_name = ds.literal(ds.first_source_table)
|
|
127
127
|
reflections = {}
|
|
128
|
-
|
|
128
|
+
allow_missing_columns = db_schema.select{|col, sch| sch[:allow_null] == false && nil != sch[:default]}.map(&:first)
|
|
129
|
+
@constraint_validations = (Sequel.synchronize{hash[table_name]} || []).map{|r| constraint_validation_array(r, reflections, allow_missing_columns)}
|
|
129
130
|
@constraint_validation_reflections = reflections
|
|
130
131
|
end
|
|
131
132
|
end
|
|
132
133
|
|
|
133
134
|
# Given a specific database constraint validation metadata row hash, transform
|
|
134
135
|
# it in an validation method call array suitable for splatting to send.
|
|
135
|
-
def constraint_validation_array(r, reflections)
|
|
136
|
+
def constraint_validation_array(r, reflections, allow_missing_columns=EMPTY_ARRAY)
|
|
136
137
|
opts = {}
|
|
137
138
|
opts[:message] = r[:message] if r[:message]
|
|
138
139
|
opts[:allow_nil] = true if db.typecast_value(:boolean, r[:allow_nil])
|
|
@@ -191,11 +192,13 @@ module Sequel
|
|
|
191
192
|
reflection_opts[:argument] = arg
|
|
192
193
|
end
|
|
193
194
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
195
|
+
opts[:from] = :values
|
|
196
|
+
if column.is_a?(Symbol) && allow_missing_columns.include?(column)
|
|
197
|
+
opts[:allow_missing] = true
|
|
197
198
|
end
|
|
198
199
|
|
|
200
|
+
a << column << opts
|
|
201
|
+
|
|
199
202
|
if column.is_a?(Array) && column.length == 1
|
|
200
203
|
column = column.first
|
|
201
204
|
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen-string-literal: true
|
|
2
2
|
|
|
3
|
+
require 'delegate'
|
|
4
|
+
|
|
3
5
|
module Sequel
|
|
4
6
|
module Plugins
|
|
5
7
|
# The defaults_setter plugin makes the column getter methods return the default
|
|
@@ -106,6 +108,20 @@ module Sequel
|
|
|
106
108
|
lambda{Date.today}
|
|
107
109
|
when Sequel::CURRENT_TIMESTAMP
|
|
108
110
|
lambda{dataset.current_datetime}
|
|
111
|
+
when Hash, Array
|
|
112
|
+
v = Marshal.dump(v).freeze
|
|
113
|
+
lambda{Marshal.load(v)}
|
|
114
|
+
when Delegator
|
|
115
|
+
# DelegateClass returns an anonymous case, which cannot be marshalled, so marshal the
|
|
116
|
+
# underlying object and create a new instance of the class with the unmarshalled object.
|
|
117
|
+
klass = v.class
|
|
118
|
+
case o = v.__getobj__
|
|
119
|
+
when Hash, Array
|
|
120
|
+
v = Marshal.dump(o).freeze
|
|
121
|
+
lambda{klass.new(Marshal.load(v))}
|
|
122
|
+
else
|
|
123
|
+
v
|
|
124
|
+
end
|
|
109
125
|
else
|
|
110
126
|
v
|
|
111
127
|
end
|
data/lib/sequel/plugins/dirty.rb
CHANGED
|
@@ -97,7 +97,9 @@ module Sequel
|
|
|
97
97
|
# Artist.first_by_name(nil)
|
|
98
98
|
# # WHERE (name IS NULL)
|
|
99
99
|
#
|
|
100
|
-
# See Dataset::PlaceholderLiteralizer for additional caveats.
|
|
100
|
+
# See Dataset::PlaceholderLiteralizer for additional caveats. Note that if the model's
|
|
101
|
+
# dataset does not support placeholder literalizers, you will not be able to use this
|
|
102
|
+
# method.
|
|
101
103
|
def finder(meth=OPTS, opts=OPTS, &block)
|
|
102
104
|
if block
|
|
103
105
|
raise Error, "cannot pass both a method name argument and a block of Model.finder" unless meth.is_a?(Hash)
|
|
@@ -149,7 +151,7 @@ module Sequel
|
|
|
149
151
|
ds
|
|
150
152
|
end
|
|
151
153
|
|
|
152
|
-
|
|
154
|
+
model.dataset.placeholder_literalizer_class.loader(model, &block)
|
|
153
155
|
end
|
|
154
156
|
end
|
|
155
157
|
|
|
@@ -24,7 +24,7 @@ module Sequel
|
|
|
24
24
|
# # Make the Album class support input transformers
|
|
25
25
|
# Album.plugin :input_transformer
|
|
26
26
|
module InputTransformer
|
|
27
|
-
def self.apply(model,
|
|
27
|
+
def self.apply(model, *, &_)
|
|
28
28
|
model.instance_exec do
|
|
29
29
|
@input_transformers = {}
|
|
30
30
|
@skip_input_transformer_columns = {}
|
data/lib/sequel/plugins/list.rb
CHANGED
|
@@ -185,10 +185,13 @@ module Sequel
|
|
|
185
185
|
end
|
|
186
186
|
|
|
187
187
|
# Set the value of the position_field to the maximum value plus 1 unless the
|
|
188
|
-
# position field already has a value.
|
|
188
|
+
# position field already has a value. If the list is empty, the position will
|
|
189
|
+
# be set to the model's +top_of_list+ value.
|
|
189
190
|
def before_validation
|
|
190
191
|
unless get_column_value(position_field)
|
|
191
|
-
|
|
192
|
+
current_max = list_dataset.max(position_field)
|
|
193
|
+
value = current_max.nil? ? model.top_of_list : current_max.to_i + 1
|
|
194
|
+
set_column_value("#{position_field}=", value)
|
|
192
195
|
end
|
|
193
196
|
super
|
|
194
197
|
end
|
|
@@ -385,7 +385,7 @@ module Sequel
|
|
|
385
385
|
iq = nil
|
|
386
386
|
end
|
|
387
387
|
fe = opts.final_edge
|
|
388
|
-
ds.graph(opts
|
|
388
|
+
ds.graph(eager_graph_dataset(opts, eo), use_only_conditions ? only_conditions : (Array(opts.right_primary_key).zip(Array(fe[:left])) + conditions), :select=>select, :table_alias=>eo[:table_alias], :qualify=>:deep, :join_type=>eo[:join_type]||join_type, :join_only=>eo[:join_only], &graph_block)
|
|
389
389
|
end
|
|
390
390
|
end
|
|
391
391
|
end
|
|
@@ -26,57 +26,27 @@ module Sequel
|
|
|
26
26
|
module MssqlOptimisticLocking
|
|
27
27
|
# Load the instance_filters plugin into the model.
|
|
28
28
|
def self.apply(model, opts=OPTS)
|
|
29
|
-
model.plugin
|
|
29
|
+
model.plugin(:optimistic_locking_base)
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
-
# Set the
|
|
32
|
+
# Set the lock column
|
|
33
33
|
def self.configure(model, opts=OPTS)
|
|
34
|
-
model.lock_column = opts[:lock_column] || :timestamp
|
|
34
|
+
model.lock_column = opts[:lock_column] || model.lock_column || :timestamp
|
|
35
35
|
end
|
|
36
|
-
|
|
37
|
-
module ClassMethods
|
|
38
|
-
# The timestamp/rowversion column containing the version for the current row.
|
|
39
|
-
attr_accessor :lock_column
|
|
40
|
-
|
|
41
|
-
Plugins.inherited_instance_variables(self, :@lock_column=>nil)
|
|
42
|
-
end
|
|
43
|
-
|
|
36
|
+
|
|
44
37
|
module InstanceMethods
|
|
45
|
-
# Add the lock column instance filter to the object before destroying it.
|
|
46
|
-
def before_destroy
|
|
47
|
-
lock_column_instance_filter
|
|
48
|
-
super
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
# Add the lock column instance filter to the object before updating it.
|
|
52
|
-
def before_update
|
|
53
|
-
lock_column_instance_filter
|
|
54
|
-
super
|
|
55
|
-
end
|
|
56
|
-
|
|
57
38
|
private
|
|
58
39
|
|
|
59
|
-
#
|
|
60
|
-
def
|
|
61
|
-
|
|
62
|
-
instance_filter(lc=>Sequel.blob(get_column_value(lc)))
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
# Clear the instance filters when refreshing, so that attempting to
|
|
66
|
-
# refresh after a failed save removes the previous lock column filter
|
|
67
|
-
# (the new one will be added before updating).
|
|
68
|
-
def _refresh(ds)
|
|
69
|
-
clear_instance_filters
|
|
70
|
-
super
|
|
40
|
+
# Make the instance filter value a blob.
|
|
41
|
+
def lock_column_instance_filter_value
|
|
42
|
+
Sequel.blob(super)
|
|
71
43
|
end
|
|
72
44
|
|
|
73
45
|
# Remove the lock column from the columns to update.
|
|
74
46
|
# SQL Server automatically updates the lock column value, and does not like
|
|
75
47
|
# it to be assigned.
|
|
76
48
|
def _save_update_all_columns_hash
|
|
77
|
-
v =
|
|
78
|
-
cc = changed_columns
|
|
79
|
-
Array(primary_key).each{|x| v.delete(x) unless cc.include?(x)}
|
|
49
|
+
v = super
|
|
80
50
|
v.delete(model.lock_column)
|
|
81
51
|
v
|
|
82
52
|
end
|
|
@@ -33,7 +33,7 @@ module Sequel
|
|
|
33
33
|
# objects. You just need to make sure that the primary key field is filled in for the
|
|
34
34
|
# associated object:
|
|
35
35
|
#
|
|
36
|
-
# a.update(:
|
|
36
|
+
# a.update(albums_attributes: [{id: 1, name: 'T'}])
|
|
37
37
|
#
|
|
38
38
|
# Since the primary key field is filled in, the plugin will update the album with id 1 instead
|
|
39
39
|
# of creating a new album.
|
|
@@ -42,14 +42,14 @@ module Sequel
|
|
|
42
42
|
# entry to the hash, and also pass the :destroy option when calling +nested_attributes+:
|
|
43
43
|
#
|
|
44
44
|
# Artist.nested_attributes :albums, destroy: true
|
|
45
|
-
# a.update(:
|
|
45
|
+
# a.update(albums_attributes: [{id: 1, _delete: true}])
|
|
46
46
|
#
|
|
47
47
|
# This will delete the related associated object from the database. If you want to leave the
|
|
48
48
|
# associated object in the database, but just remove it from the association, add a _remove
|
|
49
49
|
# entry in the hash, and also pass the :remove option when calling +nested_attributes+:
|
|
50
50
|
#
|
|
51
51
|
# Artist.nested_attributes :albums, remove: true
|
|
52
|
-
# a.update(:
|
|
52
|
+
# a.update(albums_attributes: [{id: 1, _remove: true}])
|
|
53
53
|
#
|
|
54
54
|
# The above example was for a one_to_many association, but the plugin also works similarly
|
|
55
55
|
# for other association types. For one_to_one and many_to_one associations, you need to
|
|
@@ -84,7 +84,7 @@ module Sequel
|
|
|
84
84
|
# nested attributes options for that association. This is useful for per-call filtering
|
|
85
85
|
# of the allowed fields:
|
|
86
86
|
#
|
|
87
|
-
# a.set_nested_attributes(:albums, params['artist'], :
|
|
87
|
+
# a.set_nested_attributes(:albums, params['artist'], fields: %w'name')
|
|
88
88
|
module NestedAttributes
|
|
89
89
|
# Depend on the validate_associated plugin.
|
|
90
90
|
def self.apply(model)
|
|
@@ -12,64 +12,31 @@ module Sequel
|
|
|
12
12
|
# p1 = Person[1]
|
|
13
13
|
# p2 = Person[1]
|
|
14
14
|
# p1.update(name: 'Jim') # works
|
|
15
|
-
# p2.update(name: 'Bob') # raises Sequel::
|
|
15
|
+
# p2.update(name: 'Bob') # raises Sequel::NoExistingObject
|
|
16
16
|
#
|
|
17
17
|
# In order for this plugin to work, you need to make sure that the database
|
|
18
|
-
# table has a +lock_version+ column
|
|
19
|
-
#
|
|
18
|
+
# table has a +lock_version+ column that defaults to 0. To change the column
|
|
19
|
+
# used, provide a +:lock_column+ option when loading the plugin:
|
|
20
|
+
#
|
|
21
|
+
# plugin :optimistic_locking, lock_column: :version
|
|
20
22
|
#
|
|
21
23
|
# This plugin relies on the instance_filters plugin.
|
|
22
24
|
module OptimisticLocking
|
|
23
25
|
# Exception class raised when trying to update or destroy a stale object.
|
|
24
26
|
Error = Sequel::NoExistingObject
|
|
25
27
|
|
|
26
|
-
# Load the instance_filters plugin into the model.
|
|
27
28
|
def self.apply(model, opts=OPTS)
|
|
28
|
-
model.plugin
|
|
29
|
+
model.plugin(:optimistic_locking_base)
|
|
29
30
|
end
|
|
30
31
|
|
|
31
|
-
# Set the
|
|
32
|
-
# that option is not given.
|
|
32
|
+
# Set the lock column
|
|
33
33
|
def self.configure(model, opts=OPTS)
|
|
34
|
-
model.lock_column = opts[:lock_column] || :lock_version
|
|
34
|
+
model.lock_column = opts[:lock_column] || model.lock_column || :lock_version
|
|
35
35
|
end
|
|
36
|
-
|
|
37
|
-
module ClassMethods
|
|
38
|
-
# The column holding the version of the lock
|
|
39
|
-
attr_accessor :lock_column
|
|
40
|
-
|
|
41
|
-
Plugins.inherited_instance_variables(self, :@lock_column=>nil)
|
|
42
|
-
end
|
|
43
|
-
|
|
36
|
+
|
|
44
37
|
module InstanceMethods
|
|
45
|
-
# Add the lock column instance filter to the object before destroying it.
|
|
46
|
-
def before_destroy
|
|
47
|
-
lock_column_instance_filter
|
|
48
|
-
super
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
# Add the lock column instance filter to the object before updating it.
|
|
52
|
-
def before_update
|
|
53
|
-
lock_column_instance_filter
|
|
54
|
-
super
|
|
55
|
-
end
|
|
56
|
-
|
|
57
38
|
private
|
|
58
39
|
|
|
59
|
-
# Add the lock column instance filter to the object.
|
|
60
|
-
def lock_column_instance_filter
|
|
61
|
-
lc = model.lock_column
|
|
62
|
-
instance_filter(lc=>get_column_value(lc))
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
# Clear the instance filters when refreshing, so that attempting to
|
|
66
|
-
# refresh after a failed save removes the previous lock column filter
|
|
67
|
-
# (the new one will be added before updating).
|
|
68
|
-
def _refresh(ds)
|
|
69
|
-
clear_instance_filters
|
|
70
|
-
super
|
|
71
|
-
end
|
|
72
|
-
|
|
73
40
|
# Only update the row if it has the same lock version, and increment the
|
|
74
41
|
# lock version.
|
|
75
42
|
def _update_columns(columns)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen-string-literal: true
|
|
2
|
+
|
|
3
|
+
module Sequel
|
|
4
|
+
module Plugins
|
|
5
|
+
# Base for other optimistic locking plugins
|
|
6
|
+
module OptimisticLockingBase
|
|
7
|
+
# Load the instance_filters plugin into the model.
|
|
8
|
+
def self.apply(model)
|
|
9
|
+
model.plugin :instance_filters
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
module ClassMethods
|
|
13
|
+
# The column holding the version of the lock
|
|
14
|
+
attr_accessor :lock_column
|
|
15
|
+
|
|
16
|
+
Plugins.inherited_instance_variables(self, :@lock_column=>nil)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
module InstanceMethods
|
|
20
|
+
# Add the lock column instance filter to the object before destroying it.
|
|
21
|
+
def before_destroy
|
|
22
|
+
lock_column_instance_filter
|
|
23
|
+
super
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Add the lock column instance filter to the object before updating it.
|
|
27
|
+
def before_update
|
|
28
|
+
lock_column_instance_filter
|
|
29
|
+
super
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
# Add the lock column instance filter to the object.
|
|
35
|
+
def lock_column_instance_filter
|
|
36
|
+
instance_filter(model.lock_column=>lock_column_instance_filter_value)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Use the current value of the lock column
|
|
40
|
+
def lock_column_instance_filter_value
|
|
41
|
+
public_send(model.lock_column)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Clear the instance filters when refreshing, so that attempting to
|
|
45
|
+
# refresh after a failed save removes the previous lock column filter
|
|
46
|
+
# (the new one will be added before updating).
|
|
47
|
+
def _refresh(ds)
|
|
48
|
+
clear_instance_filters
|
|
49
|
+
super
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
@@ -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
|
|
@@ -28,11 +28,13 @@ module Sequel
|
|
|
28
28
|
#
|
|
29
29
|
# This plugin only works on the postgres adapter when using the pg 0.16+ driver,
|
|
30
30
|
# PostgreSQL 9.3+ server, and PostgreSQL 9.3+ client library (libpq). In other cases
|
|
31
|
-
# it will be a no-op.
|
|
31
|
+
# it will be a no-op. Additionally, the plugin only handles models that select
|
|
32
|
+
# from tables. It does not handle models that select from subqueries, such as
|
|
33
|
+
# subclasses of models using the class_table_inheritance plugin.
|
|
32
34
|
#
|
|
33
35
|
# Example:
|
|
34
36
|
#
|
|
35
|
-
# album = Album.new(:
|
|
37
|
+
# album = Album.new(artist_id: 1) # Assume no such artist exists
|
|
36
38
|
# begin
|
|
37
39
|
# album.save
|
|
38
40
|
# rescue Sequel::ValidationFailed
|
|
@@ -131,7 +133,11 @@ module Sequel
|
|
|
131
133
|
# Dump the in-memory cached metadata to the cache file.
|
|
132
134
|
def dump_pg_auto_constraint_validations_cache
|
|
133
135
|
raise Error, "No pg_auto_constraint_validations setup" unless file = @pg_auto_constraint_validations_cache_file
|
|
134
|
-
|
|
136
|
+
pg_auto_constraint_validations_cache = {}
|
|
137
|
+
@pg_auto_constraint_validations_cache.sort.each do |k, v|
|
|
138
|
+
pg_auto_constraint_validations_cache[k] = v
|
|
139
|
+
end
|
|
140
|
+
File.open(file, 'wb'){|f| f.write(Marshal.dump(pg_auto_constraint_validations_cache))}
|
|
135
141
|
nil
|
|
136
142
|
end
|
|
137
143
|
|