sequel 5.22.0 → 5.27.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG +78 -0
- data/README.rdoc +1 -1
- data/doc/dataset_filtering.rdoc +15 -0
- data/doc/opening_databases.rdoc +3 -0
- data/doc/postgresql.rdoc +2 -2
- data/doc/release_notes/5.23.0.txt +56 -0
- data/doc/release_notes/5.24.0.txt +56 -0
- data/doc/release_notes/5.25.0.txt +32 -0
- data/doc/release_notes/5.26.0.txt +35 -0
- data/doc/release_notes/5.27.0.txt +21 -0
- data/doc/testing.rdoc +1 -0
- data/lib/sequel/adapters/jdbc.rb +7 -1
- data/lib/sequel/adapters/jdbc/postgresql.rb +1 -13
- data/lib/sequel/adapters/jdbc/sqlite.rb +29 -0
- data/lib/sequel/adapters/mysql2.rb +0 -1
- data/lib/sequel/adapters/shared/mssql.rb +9 -8
- data/lib/sequel/adapters/shared/postgres.rb +30 -7
- data/lib/sequel/adapters/shared/sqlite.rb +23 -4
- data/lib/sequel/adapters/tinytds.rb +12 -0
- data/lib/sequel/adapters/utils/mysql_mysql2.rb +1 -1
- data/lib/sequel/database/logging.rb +7 -1
- data/lib/sequel/database/schema_generator.rb +11 -2
- data/lib/sequel/database/schema_methods.rb +2 -0
- data/lib/sequel/dataset/actions.rb +3 -2
- data/lib/sequel/dataset/features.rb +6 -0
- data/lib/sequel/dataset/sql.rb +17 -4
- data/lib/sequel/extensions/named_timezones.rb +51 -9
- data/lib/sequel/extensions/pg_array.rb +4 -0
- data/lib/sequel/extensions/pg_array_ops.rb +10 -6
- data/lib/sequel/extensions/pg_enum.rb +4 -1
- data/lib/sequel/extensions/pg_json.rb +88 -17
- data/lib/sequel/extensions/pg_json_ops.rb +124 -0
- data/lib/sequel/extensions/pg_range.rb +9 -0
- data/lib/sequel/extensions/pg_row.rb +3 -1
- data/lib/sequel/extensions/sql_comments.rb +2 -2
- data/lib/sequel/model/base.rb +12 -5
- data/lib/sequel/plugins/association_multi_add_remove.rb +83 -0
- data/lib/sequel/plugins/association_proxies.rb +3 -2
- data/lib/sequel/plugins/caching.rb +3 -0
- data/lib/sequel/plugins/class_table_inheritance.rb +10 -0
- data/lib/sequel/plugins/csv_serializer.rb +26 -9
- data/lib/sequel/plugins/dirty.rb +3 -9
- data/lib/sequel/plugins/insert_conflict.rb +72 -0
- data/lib/sequel/plugins/nested_attributes.rb +7 -0
- data/lib/sequel/plugins/pg_auto_constraint_validations.rb +89 -30
- data/lib/sequel/plugins/sharding.rb +11 -5
- data/lib/sequel/plugins/static_cache.rb +8 -3
- data/lib/sequel/plugins/static_cache_cache.rb +53 -0
- data/lib/sequel/plugins/typecast_on_load.rb +3 -2
- data/lib/sequel/sql.rb +4 -1
- data/lib/sequel/timezones.rb +50 -11
- data/lib/sequel/version.rb +1 -1
- data/spec/adapters/postgres_spec.rb +135 -7
- data/spec/adapters/sqlite_spec.rb +1 -1
- data/spec/bin_spec.rb +2 -2
- data/spec/core/database_spec.rb +50 -0
- data/spec/core/dataset_spec.rb +23 -1
- data/spec/core/expression_filters_spec.rb +22 -3
- data/spec/core/schema_spec.rb +18 -0
- data/spec/core/spec_helper.rb +1 -1
- data/spec/core_extensions_spec.rb +1 -1
- data/spec/extensions/association_multi_add_remove_spec.rb +1041 -0
- data/spec/extensions/dirty_spec.rb +33 -0
- data/spec/extensions/insert_conflict_spec.rb +103 -0
- data/spec/extensions/named_timezones_spec.rb +109 -2
- data/spec/extensions/nested_attributes_spec.rb +48 -0
- data/spec/extensions/pg_array_ops_spec.rb +3 -3
- data/spec/extensions/pg_auto_constraint_validations_spec.rb +37 -0
- data/spec/extensions/pg_json_ops_spec.rb +67 -0
- data/spec/extensions/pg_json_spec.rb +12 -0
- data/spec/extensions/pg_range_spec.rb +19 -2
- data/spec/extensions/sharding_spec.rb +8 -0
- data/spec/extensions/spec_helper.rb +9 -2
- data/spec/extensions/static_cache_cache_spec.rb +35 -0
- data/spec/guards_helper.rb +1 -1
- data/spec/integration/dataset_test.rb +25 -0
- data/spec/integration/plugin_test.rb +28 -1
- data/spec/integration/schema_test.rb +16 -2
- data/spec/integration/spec_helper.rb +7 -1
- data/spec/model/spec_helper.rb +1 -1
- metadata +32 -2
data/lib/sequel/model/base.rb
CHANGED
@@ -1069,7 +1069,7 @@ module Sequel
|
|
1069
1069
|
@new = true
|
1070
1070
|
@modified = true
|
1071
1071
|
initialize_set(values)
|
1072
|
-
|
1072
|
+
_clear_changed_columns(:initialize)
|
1073
1073
|
yield self if block_given?
|
1074
1074
|
end
|
1075
1075
|
|
@@ -1626,6 +1626,13 @@ module Sequel
|
|
1626
1626
|
def _changed_columns
|
1627
1627
|
@changed_columns ||= []
|
1628
1628
|
end
|
1629
|
+
|
1630
|
+
# Clear the changed columns. Reason is the reason for clearing
|
1631
|
+
# the columns, and should be one of: :initialize, :refresh, :create
|
1632
|
+
# or :update.
|
1633
|
+
def _clear_changed_columns(_reason)
|
1634
|
+
_changed_columns.clear
|
1635
|
+
end
|
1629
1636
|
|
1630
1637
|
# Do the deletion of the object's dataset, and check that the row
|
1631
1638
|
# was actually deleted.
|
@@ -1716,7 +1723,7 @@ module Sequel
|
|
1716
1723
|
# is used for reading newly inserted values from the database
|
1717
1724
|
def _refresh(dataset)
|
1718
1725
|
_refresh_set_values(_refresh_get(dataset) || raise(NoExistingObject, "Record not found"))
|
1719
|
-
|
1726
|
+
_clear_changed_columns(:refresh)
|
1720
1727
|
end
|
1721
1728
|
|
1722
1729
|
# Get the row of column data from the database.
|
@@ -1754,7 +1761,7 @@ module Sequel
|
|
1754
1761
|
@this = nil
|
1755
1762
|
@new = false
|
1756
1763
|
@modified = false
|
1757
|
-
pk ? _save_refresh :
|
1764
|
+
pk ? _save_refresh : _clear_changed_columns(:create)
|
1758
1765
|
after_create
|
1759
1766
|
true
|
1760
1767
|
end
|
@@ -1771,7 +1778,7 @@ module Sequel
|
|
1771
1778
|
cc.clear
|
1772
1779
|
else
|
1773
1780
|
columns_updated = _save_update_all_columns_hash
|
1774
|
-
|
1781
|
+
_clear_changed_columns(:update)
|
1775
1782
|
end
|
1776
1783
|
else # update only the specified columns
|
1777
1784
|
columns = Array(columns)
|
@@ -1798,7 +1805,7 @@ module Sequel
|
|
1798
1805
|
# can be overridden to avoid the refresh.
|
1799
1806
|
def _save_refresh
|
1800
1807
|
_save_set_values(_refresh_get(this.server?(:default)) || raise(NoExistingObject, "Record not found"))
|
1801
|
-
|
1808
|
+
_clear_changed_columns(:create)
|
1802
1809
|
end
|
1803
1810
|
|
1804
1811
|
# Set values to the provided hash. Called after a create,
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
module Sequel
|
4
|
+
module Plugins
|
5
|
+
# The association_multi_add_remove plugin allows adding, removing and setting
|
6
|
+
# multiple associated objects in a single method call.
|
7
|
+
# By default Sequel::Model defines singular <tt>add_*</tt> and <tt>remove_*</tt>
|
8
|
+
# methods that operate on a single associated object, this adds plural forms
|
9
|
+
# that operate on multiple associated objects. Example:
|
10
|
+
#
|
11
|
+
# artist.albums # => [album1]
|
12
|
+
# artist.add_albums([album2, album3])
|
13
|
+
# artist.albums # => [album1, album2, album3]
|
14
|
+
# artist.remove_albums([album3, album1])
|
15
|
+
# artist.albums # => [album2]
|
16
|
+
# artist.albums = [album2, album3]
|
17
|
+
# artist.albums # => [album2, album3]
|
18
|
+
#
|
19
|
+
# It can handle all situations that the normal singular methods handle, but there is
|
20
|
+
# no attempt to optimize behavior, so using these methods will not improve performance.
|
21
|
+
#
|
22
|
+
# The add/remove/set methods defined by this plugin use a transaction,
|
23
|
+
# so if one add/remove/set fails and raises an exception, all adds/removes/set
|
24
|
+
# will be rolled back. If you are using database sharding and want to save
|
25
|
+
# to a specific shard, call Model#set_server to set the server for this instance,
|
26
|
+
# as the transaction will be opened on that server.
|
27
|
+
#
|
28
|
+
# You can customize the method names used for adding/removing multiple associated
|
29
|
+
# objects using the :multi_add_method and :multi_remove_method association options.
|
30
|
+
#
|
31
|
+
# Usage:
|
32
|
+
#
|
33
|
+
# # Allow adding/removing/setting multiple associated objects in a single call
|
34
|
+
# # for all model subclass instances (called before loading subclasses):
|
35
|
+
# Sequel::Model.plugin :association_multi_add_remove
|
36
|
+
#
|
37
|
+
# # Allow adding/removing/setting multiple associated objects in a single call
|
38
|
+
# # for Album instances (called before defining associations in the class):
|
39
|
+
# Album.plugin :association_multi_add_remove
|
40
|
+
module AssociationMultiAddRemove
|
41
|
+
module ClassMethods
|
42
|
+
# Define the methods use to add/remove/set multiple associated objects
|
43
|
+
# in a single method call.
|
44
|
+
def def_association_instance_methods(opts)
|
45
|
+
super
|
46
|
+
|
47
|
+
if opts[:adder]
|
48
|
+
add_method = opts[:add_method]
|
49
|
+
multi_add_method = opts[:multi_add_method] || :"add_#{opts[:name]}"
|
50
|
+
multi_add_method = nil if add_method == multi_add_method
|
51
|
+
if multi_add_method
|
52
|
+
association_module_def(multi_add_method, opts) do |objs, *args|
|
53
|
+
db.transaction(:server=>@server){objs.map{|obj| send(add_method, obj, *args)}.compact}
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
if opts[:remover]
|
59
|
+
remove_method = opts[:remove_method]
|
60
|
+
multi_remove_method = opts[:multi_remove_method] || :"remove_#{opts[:name]}"
|
61
|
+
multi_remove_method = nil if remove_method == multi_remove_method
|
62
|
+
if multi_remove_method
|
63
|
+
association_module_def(multi_remove_method, opts) do |objs, *args|
|
64
|
+
db.transaction(:server=>@server){objs.map{|obj| send(remove_method, obj, *args)}.compact}
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
if multi_add_method && multi_remove_method
|
70
|
+
association_module_def(:"#{opts[:name]}=", opts) do |objs, *args|
|
71
|
+
db.transaction(:server=>@server) do
|
72
|
+
existing_objs = send(opts.association_method)
|
73
|
+
send(multi_remove_method, (existing_objs - objs), *args)
|
74
|
+
send(multi_add_method, (objs - existing_objs), *args)
|
75
|
+
nil
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -64,6 +64,8 @@ module Sequel
|
|
64
64
|
array = [].freeze
|
65
65
|
|
66
66
|
if RUBY_VERSION < '2.6'
|
67
|
+
# :nocov:
|
68
|
+
|
67
69
|
# Default proc used to determine whether to send the method to the dataset.
|
68
70
|
# If the array would respond to it, sends it to the array instead of the dataset.
|
69
71
|
DEFAULT_PROXY_TO_DATASET = proc do |opts|
|
@@ -73,10 +75,9 @@ module Sequel
|
|
73
75
|
end
|
74
76
|
!array_method
|
75
77
|
end
|
76
|
-
else
|
77
78
|
# :nocov:
|
79
|
+
else
|
78
80
|
DEFAULT_PROXY_TO_DATASET = proc{|opts| !array.respond_to?(opts[:method])}
|
79
|
-
# :nocov:
|
80
81
|
end
|
81
82
|
|
82
83
|
# Set the association reflection to use, and whether the association should be
|
@@ -26,6 +26,9 @@ module Sequel
|
|
26
26
|
# * Model.with_pk!
|
27
27
|
# * Model.[] # when argument is not hash or nil
|
28
28
|
# * many_to_one association method # without dynamic callback, when primary key matches
|
29
|
+
#
|
30
|
+
# You should not use this plugin if you are using sharding and there are different
|
31
|
+
# rows for the same primary key on different shards.
|
29
32
|
#
|
30
33
|
# Usage:
|
31
34
|
#
|
@@ -108,6 +108,16 @@ module Sequel
|
|
108
108
|
# a = Executive.where{{employees[:id]=>1}}.first # works
|
109
109
|
# a = Executive.where{{executives[:id]=>1}}.first # doesn't work
|
110
110
|
#
|
111
|
+
# Note that because subclass datasets select from a subquery, you cannot update,
|
112
|
+
# delete, or insert into them directly. To delete related rows, you need to go
|
113
|
+
# through the related tables and remove the related rows. Code that does this would
|
114
|
+
# be similar to:
|
115
|
+
#
|
116
|
+
# pks = Executive.where{num_staff < 10}.select_map(:id)
|
117
|
+
# Executive.cti_tables.reverse_each do |table|
|
118
|
+
# DB.from(table).where(:id=>pks).delete
|
119
|
+
# end
|
120
|
+
#
|
111
121
|
# = Usage
|
112
122
|
#
|
113
123
|
# # Use the default of storing the class name in the sti_key
|
@@ -65,8 +65,6 @@ module Sequel
|
|
65
65
|
# # Add CSV output capability to Album class instances
|
66
66
|
# Album.plugin :csv_serializer
|
67
67
|
module CsvSerializer
|
68
|
-
CSV = Object.const_defined?(:CSV) ? ::CSV : ::FasterCSV
|
69
|
-
|
70
68
|
# Set up the column readers to do deserialization and the column writers
|
71
69
|
# to save the value in deserialized_values
|
72
70
|
def self.configure(model, opts = OPTS)
|
@@ -75,13 +73,29 @@ module Sequel
|
|
75
73
|
end
|
76
74
|
end
|
77
75
|
|
76
|
+
# Avoid keyword argument separation warnings on Ruby 2.7, while still
|
77
|
+
# being compatible with 1.9.
|
78
|
+
if RUBY_VERSION >= "2.0"
|
79
|
+
instance_eval(<<-END, __FILE__, __LINE__+1)
|
80
|
+
def self.csv_call(*args, opts, &block)
|
81
|
+
CSV.send(*args, **opts, &block)
|
82
|
+
end
|
83
|
+
END
|
84
|
+
else
|
85
|
+
# :nodoc:
|
86
|
+
def self.csv_call(*args, opts, &block)
|
87
|
+
CSV.send(*args, opts, &block)
|
88
|
+
end
|
89
|
+
# :nodoc:
|
90
|
+
end
|
91
|
+
|
78
92
|
module ClassMethods
|
79
93
|
# The default opts to use when serializing model objects to CSV
|
80
94
|
attr_reader :csv_serializer_opts
|
81
95
|
|
82
96
|
# Attempt to parse an array of instances from the given CSV string
|
83
97
|
def array_from_csv(csv, opts = OPTS)
|
84
|
-
|
98
|
+
CsvSerializer.csv_call(:parse, csv, process_csv_serializer_opts(opts)).map do |row|
|
85
99
|
row = row.to_hash
|
86
100
|
row.delete(nil)
|
87
101
|
new(row)
|
@@ -108,7 +122,8 @@ module Sequel
|
|
108
122
|
opts_cols = opts.delete(:columns)
|
109
123
|
opts_include = opts.delete(:include)
|
110
124
|
opts_except = opts.delete(:except)
|
111
|
-
|
125
|
+
only = opts.delete(:only)
|
126
|
+
opts[:headers] ||= Array(only || opts_cols || columns) + Array(opts_include) - Array(opts_except)
|
112
127
|
opts
|
113
128
|
end
|
114
129
|
|
@@ -130,7 +145,7 @@ module Sequel
|
|
130
145
|
# :headers :: The headers to use for the CSV line. Use nil for a header
|
131
146
|
# to specify the column should be ignored.
|
132
147
|
def from_csv(csv, opts = OPTS)
|
133
|
-
row =
|
148
|
+
row = CsvSerializer.csv_call(:parse_line, csv, model.process_csv_serializer_opts(opts)).to_hash
|
134
149
|
row.delete(nil)
|
135
150
|
set(row)
|
136
151
|
end
|
@@ -146,9 +161,10 @@ module Sequel
|
|
146
161
|
# attributes to include in the CSV output.
|
147
162
|
def to_csv(opts = OPTS)
|
148
163
|
opts = model.process_csv_serializer_opts(opts)
|
164
|
+
headers = opts[:headers]
|
149
165
|
|
150
|
-
|
151
|
-
csv <<
|
166
|
+
CsvSerializer.csv_call(:generate, model.process_csv_serializer_opts(opts)) do |csv|
|
167
|
+
csv << headers.map{|k| public_send(k)}
|
152
168
|
end
|
153
169
|
end
|
154
170
|
end
|
@@ -164,10 +180,11 @@ module Sequel
|
|
164
180
|
def to_csv(opts = OPTS)
|
165
181
|
opts = model.process_csv_serializer_opts({:columns=>columns}.merge!(opts))
|
166
182
|
items = opts.delete(:array) || self
|
183
|
+
headers = opts[:headers]
|
167
184
|
|
168
|
-
|
185
|
+
CsvSerializer.csv_call(:generate, opts) do |csv|
|
169
186
|
items.each do |object|
|
170
|
-
csv <<
|
187
|
+
csv << headers.map{|header| object.public_send(header)}
|
171
188
|
end
|
172
189
|
end
|
173
190
|
end
|
data/lib/sequel/plugins/dirty.rb
CHANGED
@@ -159,9 +159,9 @@ module Sequel
|
|
159
159
|
|
160
160
|
private
|
161
161
|
|
162
|
-
# Reset
|
163
|
-
def
|
164
|
-
reset_initial_values
|
162
|
+
# Reset initial values when clearing changed columns
|
163
|
+
def _clear_changed_columns(reason)
|
164
|
+
reset_initial_values if reason == :initialize || reason == :refresh
|
165
165
|
super
|
166
166
|
end
|
167
167
|
|
@@ -214,12 +214,6 @@ module Sequel
|
|
214
214
|
self
|
215
215
|
end
|
216
216
|
|
217
|
-
# Reset the initial values when initializing.
|
218
|
-
def initialize_set(h)
|
219
|
-
super
|
220
|
-
reset_initial_values
|
221
|
-
end
|
222
|
-
|
223
217
|
# Array holding column symbols that were not present initially. This is necessary
|
224
218
|
# to differentiate between values that were not present and values that were
|
225
219
|
# present but equal to nil.
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
module Sequel
|
4
|
+
module Plugins
|
5
|
+
# The insert_conflict plugin allows handling conflicts due to unique
|
6
|
+
# constraints when saving new model instance, using the INSERT ON CONFLICT
|
7
|
+
# support in PostgreSQL 9.5+ and SQLite 3.24.0+. Example:
|
8
|
+
#
|
9
|
+
# class Album < Sequel::Model
|
10
|
+
# plugin :insert_conflict
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
# Album.new(name: 'Foo', copies_sold: 1000).
|
14
|
+
# insert_conflict(
|
15
|
+
# target: :name,
|
16
|
+
# update: {copies_sold: Sequel[:excluded][:b]}
|
17
|
+
# ).
|
18
|
+
# save
|
19
|
+
#
|
20
|
+
# This example will try to insert the album, but if there is an existing
|
21
|
+
# album with the name 'Foo', this will update the copies_sold attribute
|
22
|
+
# for that album. See the PostgreSQL and SQLite adapter documention for
|
23
|
+
# the options you can pass to the insert_conflict method.
|
24
|
+
#
|
25
|
+
# Usage:
|
26
|
+
#
|
27
|
+
# # Make all model subclasses support insert_conflict
|
28
|
+
# Sequel::Model.plugin :insert_conflict
|
29
|
+
#
|
30
|
+
# # Make the Album class support insert_conflict
|
31
|
+
# Album.plugin :insert_conflict
|
32
|
+
module InsertConflict
|
33
|
+
def self.configure(model)
|
34
|
+
model.instance_exec do
|
35
|
+
if @dataset && !@dataset.respond_to?(:insert_conflict)
|
36
|
+
raise Error, "#{self}'s dataset does not support insert_conflict"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
module InstanceMethods
|
42
|
+
# Set the insert_conflict options to pass to the dataset when inserting.
|
43
|
+
def insert_conflict(opts=OPTS)
|
44
|
+
raise Error, "Model#insert_conflict is only supported on new model instances" unless new?
|
45
|
+
@insert_conflict_opts = opts
|
46
|
+
self
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
# Set the dataset used for inserting to use INSERT ON CONFLICT
|
52
|
+
# Model#insert_conflict has been called on the instance previously.
|
53
|
+
def _insert_dataset
|
54
|
+
ds = super
|
55
|
+
|
56
|
+
if @insert_conflict_opts
|
57
|
+
ds = ds.insert_conflict(@insert_conflict_opts)
|
58
|
+
end
|
59
|
+
|
60
|
+
ds
|
61
|
+
end
|
62
|
+
|
63
|
+
# Disable the use of prepared insert statements, as they are not compatible
|
64
|
+
# with this plugin.
|
65
|
+
def use_prepared_statements_for?(type)
|
66
|
+
return false if type == :insert || type == :insert_select
|
67
|
+
super if defined?(super)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -113,6 +113,10 @@ module Sequel
|
|
113
113
|
# value, the attribute hash is ignored.
|
114
114
|
# :remove :: Allow disassociation of nested records (can remove the associated
|
115
115
|
# object from the parent object, but not destroy the associated object).
|
116
|
+
# :require_modification :: Whether to require modification of nested objects when
|
117
|
+
# updating or deleting them (checking that a single row was
|
118
|
+
# updated). By default, uses the default require_modification
|
119
|
+
# setting for the nested object.
|
116
120
|
# :transform :: A proc to transform attribute hashes before they are
|
117
121
|
# passed to associated object. Takes two arguments, the parent object and
|
118
122
|
# the attribute hash. Uses the return value as the new attribute hash.
|
@@ -282,6 +286,9 @@ module Sequel
|
|
282
286
|
obj = Array(public_send(reflection[:name])).find{|x| Array(x.pk).map(&:to_s) == pk}
|
283
287
|
end
|
284
288
|
if obj
|
289
|
+
unless (require_modification = meta[:require_modification]).nil?
|
290
|
+
obj.require_modification = require_modification
|
291
|
+
end
|
285
292
|
attributes = attributes.dup.delete_if{|k,v| str_keys.include? k.to_s}
|
286
293
|
if meta[:destroy] && klass.db.send(:typecast_value_boolean, attributes.delete(:_delete) || attributes.delete('_delete'))
|
287
294
|
nested_attributes_remove(meta, obj, :destroy=>true)
|
@@ -45,6 +45,31 @@ module Sequel
|
|
45
45
|
# to be associated to particular column(s), and use a specific error message:
|
46
46
|
#
|
47
47
|
# Album.pg_auto_constraint_validation_override(:constraint_name, [:column1], "validation error message")
|
48
|
+
#
|
49
|
+
# Using the pg_auto_constraint_validations plugin requires 5 queries per
|
50
|
+
# model at load time in order to gather the necessary metadata. For applications
|
51
|
+
# with a large number of models, this can result in a noticeable delay during model
|
52
|
+
# initialization. To mitigate this issue, you can cache the necessary metadata in
|
53
|
+
# a file with the :cache_file option:
|
54
|
+
#
|
55
|
+
# Sequel::Model.plugin :pg_auto_constraint_validations, cache_file: 'db/pgacv.cache'
|
56
|
+
#
|
57
|
+
# The file does not have to exist when loading the plugin. If it exists, the plugin
|
58
|
+
# will load the cache and use the cached results instead of issuing queries if there
|
59
|
+
# is an entry in the cache. If there is no entry in the cache, it will update the
|
60
|
+
# in-memory cache with the metadata results. To save the in in-memory cache back to
|
61
|
+
# the cache file, run:
|
62
|
+
#
|
63
|
+
# Sequel::Model.dump_pg_auto_constraint_validations_cache
|
64
|
+
#
|
65
|
+
# Note that when using the :cache_file option, it is up to the application to ensure
|
66
|
+
# that the dumped cached metadata reflects the current state of the database. Sequel
|
67
|
+
# does no checking to ensure this, as checking would take time and the
|
68
|
+
# purpose of this code is to take a shortcut.
|
69
|
+
#
|
70
|
+
# The cached schema is dumped in Marshal format, since it is the fastest
|
71
|
+
# and it handles all ruby objects used in the metadata. Because of this,
|
72
|
+
# you should not attempt to load the metadata from a untrusted file.
|
48
73
|
#
|
49
74
|
# Usage:
|
50
75
|
#
|
@@ -67,13 +92,28 @@ module Sequel
|
|
67
92
|
}.freeze).each_value(&:freeze)
|
68
93
|
|
69
94
|
# Setup the constraint violation metadata. Options:
|
95
|
+
# :cache_file :: File storing cached metadata, to avoid queries for each model
|
70
96
|
# :messages :: Override the default error messages for each constraint
|
71
97
|
# violation type (:not_null, :check, :unique, :foreign_key, :referenced_by)
|
72
98
|
def self.configure(model, opts=OPTS)
|
73
99
|
model.instance_exec do
|
100
|
+
if @pg_auto_constraint_validations_cache_file = opts[:cache_file]
|
101
|
+
@pg_auto_constraint_validations_cache = if ::File.file?(@pg_auto_constraint_validations_cache_file)
|
102
|
+
cache = Marshal.load(File.read(@pg_auto_constraint_validations_cache_file))
|
103
|
+
cache.each_value do |hash|
|
104
|
+
hash.freeze.each_value(&:freeze)
|
105
|
+
end
|
106
|
+
else
|
107
|
+
{}
|
108
|
+
end
|
109
|
+
else
|
110
|
+
@pg_auto_constraint_validations_cache = nil
|
111
|
+
end
|
112
|
+
|
74
113
|
setup_pg_auto_constraint_validations
|
75
114
|
@pg_auto_constraint_validations_messages = (@pg_auto_constraint_validations_messages || DEFAULT_ERROR_MESSAGES).merge(opts[:messages] || OPTS).freeze
|
76
115
|
end
|
116
|
+
nil
|
77
117
|
end
|
78
118
|
|
79
119
|
module ClassMethods
|
@@ -85,9 +125,16 @@ module Sequel
|
|
85
125
|
# generated validation failures.
|
86
126
|
attr_reader :pg_auto_constraint_validations_messages
|
87
127
|
|
88
|
-
Plugins.inherited_instance_variables(self, :@pg_auto_constraint_validations=>nil, :@pg_auto_constraint_validations_messages=>nil)
|
128
|
+
Plugins.inherited_instance_variables(self, :@pg_auto_constraint_validations=>nil, :@pg_auto_constraint_validations_messages=>nil, :@pg_auto_constraint_validations_cache=>nil, :@pg_auto_constraint_validations_cache_file=>nil)
|
89
129
|
Plugins.after_set_dataset(self, :setup_pg_auto_constraint_validations)
|
90
130
|
|
131
|
+
# Dump the in-memory cached metadata to the cache file.
|
132
|
+
def dump_pg_auto_constraint_validations_cache
|
133
|
+
raise Error, "No pg_auto_constraint_validations setup" unless file = @pg_auto_constraint_validations_cache_file
|
134
|
+
File.open(file, 'wb'){|f| f.write(Marshal.dump(@pg_auto_constraint_validations_cache))}
|
135
|
+
nil
|
136
|
+
end
|
137
|
+
|
91
138
|
# Override the constraint validation columns and message for a given constraint
|
92
139
|
def pg_auto_constraint_validation_override(constraint, columns, message)
|
93
140
|
pgacv = Hash[@pg_auto_constraint_validations]
|
@@ -122,39 +169,51 @@ module Sequel
|
|
122
169
|
return
|
123
170
|
end
|
124
171
|
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
172
|
+
cache = @pg_auto_constraint_validations_cache
|
173
|
+
literal_table_name = dataset.literal(table_name)
|
174
|
+
unless cache && (metadata = cache[literal_table_name])
|
175
|
+
checks = {}
|
176
|
+
indexes = {}
|
177
|
+
foreign_keys = {}
|
178
|
+
referenced_by = {}
|
129
179
|
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
180
|
+
db.check_constraints(table_name).each do |k, v|
|
181
|
+
checks[k] = v[:columns].dup.freeze unless v[:columns].empty?
|
182
|
+
end
|
183
|
+
db.indexes(table_name, :include_partial=>true).each do |k, v|
|
184
|
+
if v[:unique]
|
185
|
+
indexes[k] = v[:columns].dup.freeze
|
186
|
+
end
|
187
|
+
end
|
188
|
+
db.foreign_key_list(table_name, :schema=>false).each do |fk|
|
189
|
+
foreign_keys[fk[:name]] = fk[:columns].dup.freeze
|
190
|
+
end
|
191
|
+
db.foreign_key_list(table_name, :reverse=>true, :schema=>false).each do |fk|
|
192
|
+
referenced_by[[fk[:schema], fk[:table], fk[:name]].freeze] = fk[:key].dup.freeze
|
193
|
+
end
|
194
|
+
|
195
|
+
schema, table = db[:pg_class].
|
196
|
+
join(:pg_namespace, :oid=>:relnamespace, db.send(:regclass_oid, table_name)=>:oid).
|
197
|
+
get([:nspname, :relname])
|
198
|
+
|
199
|
+
metadata = {
|
200
|
+
:schema=>schema,
|
201
|
+
:table=>table,
|
202
|
+
:check=>checks,
|
203
|
+
:unique=>indexes,
|
204
|
+
:foreign_key=>foreign_keys,
|
205
|
+
:referenced_by=>referenced_by,
|
206
|
+
:overrides=>OPTS
|
207
|
+
}.freeze
|
208
|
+
metadata.each_value(&:freeze)
|
209
|
+
|
210
|
+
if cache
|
211
|
+
cache[literal_table_name] = metadata
|
136
212
|
end
|
137
|
-
end
|
138
|
-
db.foreign_key_list(table_name, :schema=>false).each do |fk|
|
139
|
-
foreign_keys[fk[:name]] = fk[:columns].dup.freeze
|
140
|
-
end
|
141
|
-
db.foreign_key_list(table_name, :reverse=>true, :schema=>false).each do |fk|
|
142
|
-
referenced_by[[fk[:schema], fk[:table], fk[:name]].freeze] = fk[:key].dup.freeze
|
143
213
|
end
|
144
214
|
|
145
|
-
|
146
|
-
|
147
|
-
get([:nspname, :relname])
|
148
|
-
|
149
|
-
(@pg_auto_constraint_validations = {
|
150
|
-
:schema=>schema,
|
151
|
-
:table=>table,
|
152
|
-
:check=>checks,
|
153
|
-
:unique=>indexes,
|
154
|
-
:foreign_key=>foreign_keys,
|
155
|
-
:referenced_by=>referenced_by,
|
156
|
-
:overrides=>OPTS
|
157
|
-
}.freeze).each_value(&:freeze)
|
215
|
+
@pg_auto_constraint_validations = metadata
|
216
|
+
nil
|
158
217
|
end
|
159
218
|
end
|
160
219
|
|