sequel 5.7.1 → 5.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG +53 -1
- data/doc/association_basics.rdoc +2 -2
- data/doc/migration.rdoc +11 -10
- data/doc/postgresql.rdoc +71 -0
- data/doc/release_notes/5.8.0.txt +170 -0
- data/lib/sequel/adapters/jdbc.rb +6 -1
- data/lib/sequel/adapters/jdbc/postgresql.rb +3 -3
- data/lib/sequel/adapters/mysql2.rb +2 -1
- data/lib/sequel/adapters/postgres.rb +32 -10
- data/lib/sequel/adapters/shared/mssql.rb +11 -11
- data/lib/sequel/adapters/shared/mysql.rb +51 -6
- data/lib/sequel/adapters/shared/oracle.rb +12 -2
- data/lib/sequel/adapters/shared/postgres.rb +97 -30
- data/lib/sequel/adapters/shared/sqlanywhere.rb +2 -2
- data/lib/sequel/adapters/shared/sqlite.rb +6 -1
- data/lib/sequel/dataset/features.rb +5 -0
- data/lib/sequel/dataset/query.rb +48 -19
- data/lib/sequel/exceptions.rb +7 -0
- data/lib/sequel/extensions/connection_expiration.rb +8 -3
- data/lib/sequel/extensions/pg_enum.rb +28 -5
- data/lib/sequel/plugins/association_proxies.rb +16 -4
- data/lib/sequel/plugins/error_splitter.rb +16 -11
- data/lib/sequel/plugins/pg_auto_constraint_validations.rb +260 -0
- data/lib/sequel/plugins/subclasses.rb +1 -1
- data/lib/sequel/plugins/tactical_eager_loading.rb +1 -1
- data/lib/sequel/version.rb +2 -2
- data/spec/adapters/mysql_spec.rb +0 -1
- data/spec/adapters/postgres_spec.rb +169 -4
- data/spec/adapters/sqlite_spec.rb +13 -0
- data/spec/core/dataset_spec.rb +21 -0
- data/spec/extensions/association_proxies_spec.rb +21 -7
- data/spec/extensions/connection_expiration_spec.rb +13 -1
- data/spec/extensions/pg_auto_constraint_validations_spec.rb +165 -0
- data/spec/extensions/pg_enum_spec.rb +26 -22
- data/spec/extensions/tactical_eager_loading_spec.rb +11 -0
- data/spec/integration/dataset_test.rb +30 -6
- data/spec/integration/plugin_test.rb +2 -2
- metadata +6 -2
@@ -232,7 +232,7 @@ module Sequel
|
|
232
232
|
end
|
233
233
|
|
234
234
|
module DatasetMethods
|
235
|
-
Dataset.def_sql_method(self, :insert, %w'
|
235
|
+
Dataset.def_sql_method(self, :insert, %w'insert into columns values')
|
236
236
|
Dataset.def_sql_method(self, :select, %w'with select distinct limit columns into from join where group having compounds order lock')
|
237
237
|
|
238
238
|
# Whether to convert smallint to boolean arguments for this dataset.
|
@@ -247,7 +247,7 @@ module Sequel
|
|
247
247
|
end
|
248
248
|
|
249
249
|
def supports_cte?(type=:select)
|
250
|
-
type == :select
|
250
|
+
type == :select
|
251
251
|
end
|
252
252
|
|
253
253
|
# SQLAnywhere supports GROUPING SETS
|
@@ -179,7 +179,12 @@ module Sequel
|
|
179
179
|
fks = fetch("PRAGMA foreign_keys")
|
180
180
|
run "PRAGMA foreign_keys = 0" if fks
|
181
181
|
transaction do
|
182
|
-
if ops.length > 1 && ops.all?{|op| op[:op] == :add_constraint}
|
182
|
+
if ops.length > 1 && ops.all?{|op| op[:op] == :add_constraint || op[:op] == :set_column_null}
|
183
|
+
null_ops, ops = ops.partition{|op| op[:op] == :set_column_null}
|
184
|
+
|
185
|
+
# Apply NULL/NOT NULL ops first, since those should be purely idependent of the constraints.
|
186
|
+
null_ops.each{|op| alter_table_sql_list(table, [op]).flatten.each{|sql| execute_ddl(sql)}}
|
187
|
+
|
183
188
|
# If you are just doing constraints, apply all of them at the same time,
|
184
189
|
# as otherwise all but the last one get lost.
|
185
190
|
alter_table_sql_list(table, [{:op=>:add_constraints, :ops=>ops}]).flatten.each{|sql| execute_ddl(sql)}
|
@@ -115,6 +115,11 @@ module Sequel
|
|
115
115
|
true
|
116
116
|
end
|
117
117
|
|
118
|
+
# Whether the dataset supports skipping raising an error instead of waiting for locked rows when returning data, false by default.
|
119
|
+
def supports_nowait?
|
120
|
+
false
|
121
|
+
end
|
122
|
+
|
118
123
|
# Whether modifying joined datasets is supported, false by default.
|
119
124
|
def supports_modifying_joins?
|
120
125
|
false
|
data/lib/sequel/dataset/query.rb
CHANGED
@@ -88,7 +88,6 @@ module Sequel
|
|
88
88
|
c.clear_columns_cache
|
89
89
|
end
|
90
90
|
c.freeze
|
91
|
-
c
|
92
91
|
end
|
93
92
|
else
|
94
93
|
# :nocov:
|
@@ -116,8 +115,12 @@ module Sequel
|
|
116
115
|
# DB[:items].order(:id).distinct{func(:id)} # SQL: SELECT DISTINCT ON (func(id)) * FROM items ORDER BY id
|
117
116
|
def distinct(*args, &block)
|
118
117
|
virtual_row_columns(args, block)
|
119
|
-
|
120
|
-
|
118
|
+
if args.empty?
|
119
|
+
cached_dataset(:_distinct_ds){clone(:distinct => EMPTY_ARRAY)}
|
120
|
+
else
|
121
|
+
raise(InvalidOperation, "DISTINCT ON not supported") unless supports_distinct_on?
|
122
|
+
clone(:distinct => args.freeze)
|
123
|
+
end
|
121
124
|
end
|
122
125
|
|
123
126
|
# Adds an EXCEPT clause using a second dataset object.
|
@@ -190,7 +193,6 @@ module Sequel
|
|
190
193
|
c = _clone(:freeze=>false)
|
191
194
|
c.send(:_extension!, a)
|
192
195
|
c.freeze
|
193
|
-
c
|
194
196
|
end
|
195
197
|
else
|
196
198
|
# :nocov:
|
@@ -274,11 +276,15 @@ module Sequel
|
|
274
276
|
def from_self(opts=OPTS)
|
275
277
|
fs = {}
|
276
278
|
@opts.keys.each{|k| fs[k] = nil unless non_sql_option?(k)}
|
277
|
-
|
278
|
-
|
279
|
-
|
279
|
+
pr = proc do
|
280
|
+
c = clone(fs).from(opts[:alias] ? as(opts[:alias], opts[:column_aliases]) : self)
|
281
|
+
if cols = _columns
|
282
|
+
c.send(:columns=, cols)
|
283
|
+
end
|
284
|
+
c
|
280
285
|
end
|
281
|
-
|
286
|
+
|
287
|
+
cache ? cached_dataset(:_from_self_ds, &pr) : pr.call
|
282
288
|
end
|
283
289
|
|
284
290
|
# Match any of the columns to any of the patterns. The terms can be
|
@@ -616,7 +622,7 @@ module Sequel
|
|
616
622
|
# DB.from(:a, DB[:b].where(Sequel[:a][:c]=>Sequel[:b][:d]).lateral)
|
617
623
|
# # SELECT * FROM a, LATERAL (SELECT * FROM b WHERE (a.c = b.d))
|
618
624
|
def lateral
|
619
|
-
clone(:lateral=>true)
|
625
|
+
cached_dataset(:_lateral_ds){clone(:lateral=>true)}
|
620
626
|
end
|
621
627
|
|
622
628
|
# If given an integer, the dataset will contain only the first l results.
|
@@ -672,6 +678,18 @@ module Sequel
|
|
672
678
|
cached_dataset(:_naked_ds){with_row_proc(nil)}
|
673
679
|
end
|
674
680
|
|
681
|
+
# Returns a copy of the dataset that will raise a DatabaseLockTimeout instead
|
682
|
+
# of waiting for rows that are locked by another transaction
|
683
|
+
#
|
684
|
+
# DB[:items].for_update.nowait
|
685
|
+
# # SELECT * FROM items FOR UPDATE NOWAIT
|
686
|
+
def nowait
|
687
|
+
cached_dataset(:_nowait_ds) do
|
688
|
+
raise(Error, 'This dataset does not support raises errors instead of waiting for locked rows') unless supports_nowait?
|
689
|
+
clone(:nowait=>true)
|
690
|
+
end
|
691
|
+
end
|
692
|
+
|
675
693
|
# Returns a copy of the dataset with a specified order. Can be safely combined with limit.
|
676
694
|
# If you call limit with an offset, it will override override the offset if you've called
|
677
695
|
# offset first.
|
@@ -755,15 +773,20 @@ module Sequel
|
|
755
773
|
#
|
756
774
|
# DB[:items].where(id: 1).qualify(:i)
|
757
775
|
# # SELECT i.* FROM items WHERE (i.id = 1)
|
758
|
-
def qualify(table=first_source)
|
776
|
+
def qualify(table=(cache=true; first_source))
|
759
777
|
o = @opts
|
760
778
|
return self if o[:sql]
|
761
|
-
|
762
|
-
|
763
|
-
h
|
779
|
+
|
780
|
+
pr = proc do
|
781
|
+
h = {}
|
782
|
+
(o.keys & QUALIFY_KEYS).each do |k|
|
783
|
+
h[k] = qualified_expression(o[k], table)
|
784
|
+
end
|
785
|
+
h[:select] = [SQL::ColumnAll.new(table)].freeze if !o[:select] || o[:select].empty?
|
786
|
+
clone(h)
|
764
787
|
end
|
765
|
-
|
766
|
-
|
788
|
+
|
789
|
+
cache ? cached_dataset(:_qualify_ds, &pr) : pr.call
|
767
790
|
end
|
768
791
|
|
769
792
|
# Modify the RETURNING clause, only supported on a few databases. If returning
|
@@ -785,8 +808,15 @@ module Sequel
|
|
785
808
|
# # hash for each row deleted, with values for all columns
|
786
809
|
# end
|
787
810
|
def returning(*values)
|
788
|
-
|
789
|
-
|
811
|
+
if values.empty?
|
812
|
+
cached_dataset(:_returning_ds) do
|
813
|
+
raise Error, "RETURNING is not supported on #{db.database_type}" unless supports_returning?(:insert)
|
814
|
+
clone(:returning=>EMPTY_ARRAY)
|
815
|
+
end
|
816
|
+
else
|
817
|
+
raise Error, "RETURNING is not supported on #{db.database_type}" unless supports_returning?(:insert)
|
818
|
+
clone(:returning=>values.freeze)
|
819
|
+
end
|
790
820
|
end
|
791
821
|
|
792
822
|
# Returns a copy of the dataset with the order reversed. If no order is
|
@@ -831,7 +861,7 @@ module Sequel
|
|
831
861
|
# DB[:items].select_all(:items, :foo) # SELECT items.*, foo.* FROM items
|
832
862
|
def select_all(*tables)
|
833
863
|
if tables.empty?
|
834
|
-
clone(:select => nil)
|
864
|
+
cached_dataset(:_select_all_ds){clone(:select => nil)}
|
835
865
|
else
|
836
866
|
select(*tables.map{|t| i, a = split_alias(t); a || i}.map!{|t| SQL::ColumnAll.new(t)}.freeze)
|
837
867
|
end
|
@@ -1067,7 +1097,6 @@ module Sequel
|
|
1067
1097
|
c.extend(*mods) unless mods.empty?
|
1068
1098
|
c.extend(DatasetModule.new(&block)) if block
|
1069
1099
|
c.freeze
|
1070
|
-
c
|
1071
1100
|
end
|
1072
1101
|
else
|
1073
1102
|
# :nocov:
|
data/lib/sequel/exceptions.rb
CHANGED
@@ -73,6 +73,13 @@ module Sequel
|
|
73
73
|
SerializationFailure = Class.new(DatabaseError)
|
74
74
|
).name
|
75
75
|
|
76
|
+
(
|
77
|
+
# Error raised when Sequel determines the database could not acquire a necessary lock
|
78
|
+
# before timing out. Use of Dataset#nowait can often cause this exception when
|
79
|
+
# retrieving rows.
|
80
|
+
DatabaseLockTimeout = Class.new(DatabaseError)
|
81
|
+
).name
|
82
|
+
|
76
83
|
(
|
77
84
|
# Error raised on an invalid operation, such as trying to update or delete
|
78
85
|
# a joined or grouped dataset when the database does not support that.
|
@@ -38,12 +38,17 @@ module Sequel
|
|
38
38
|
# Defaults to 14400 seconds (4 hours).
|
39
39
|
attr_accessor :connection_expiration_timeout
|
40
40
|
|
41
|
+
# The maximum number of seconds that will be added as a random delay to the expiration timeout
|
42
|
+
# Defaults to 0 seconds (no random delay).
|
43
|
+
attr_accessor :connection_expiration_random_delay
|
44
|
+
|
41
45
|
# Initialize the data structures used by this extension.
|
42
46
|
def self.extended(pool)
|
43
47
|
pool.instance_exec do
|
44
48
|
sync do
|
45
49
|
@connection_expiration_timestamps ||= {}
|
46
50
|
@connection_expiration_timeout ||= 14400
|
51
|
+
@connection_expiration_random_delay ||= 0
|
47
52
|
end
|
48
53
|
end
|
49
54
|
end
|
@@ -59,7 +64,7 @@ module Sequel
|
|
59
64
|
# Record the time the connection was created.
|
60
65
|
def make_new(*)
|
61
66
|
conn = super
|
62
|
-
@connection_expiration_timestamps[conn] = Sequel.start_timer
|
67
|
+
@connection_expiration_timestamps[conn] = [Sequel.start_timer, @connection_expiration_timeout + (rand * @connection_expiration_random_delay)].freeze
|
63
68
|
conn
|
64
69
|
end
|
65
70
|
|
@@ -69,8 +74,8 @@ module Sequel
|
|
69
74
|
def acquire(*a)
|
70
75
|
begin
|
71
76
|
if (conn = super) &&
|
72
|
-
(
|
73
|
-
Sequel.elapsed_seconds_since(
|
77
|
+
(cet = sync{@connection_expiration_timestamps[conn]}) &&
|
78
|
+
Sequel.elapsed_seconds_since(cet[0]) > cet[1]
|
74
79
|
|
75
80
|
if pool_type == :sharded_threaded
|
76
81
|
sync{allocated(a.last).delete(Thread.current)}
|
@@ -13,6 +13,10 @@
|
|
13
13
|
#
|
14
14
|
# DB.add_enum_value(:enum_type_name, 'value4')
|
15
15
|
#
|
16
|
+
# If you want to rename an enum type, you can use rename_enum:
|
17
|
+
#
|
18
|
+
# DB.rename_enum(:enum_type_name, :enum_type_another_name)
|
19
|
+
#
|
16
20
|
# If you want to drop an enum type, you can use drop_enum:
|
17
21
|
#
|
18
22
|
# DB.drop_enum(:enum_type_name)
|
@@ -63,7 +67,10 @@ module Sequel
|
|
63
67
|
# Parse the available enum values when loading this extension into
|
64
68
|
# your database.
|
65
69
|
def self.extended(db)
|
66
|
-
db.
|
70
|
+
db.instance_exec do
|
71
|
+
@enum_labels = {}
|
72
|
+
parse_enum_labels
|
73
|
+
end
|
67
74
|
end
|
68
75
|
|
69
76
|
# Run the SQL to add the given value to the existing enum type.
|
@@ -92,6 +99,15 @@ module Sequel
|
|
92
99
|
nil
|
93
100
|
end
|
94
101
|
|
102
|
+
# Run the SQL to rename the enum type with the given name
|
103
|
+
# to the another given name.
|
104
|
+
def rename_enum(enum, new_name)
|
105
|
+
sql = "ALTER TYPE #{quote_schema_table(enum)} RENAME TO #{quote_schema_table(new_name)}"
|
106
|
+
run sql
|
107
|
+
parse_enum_labels
|
108
|
+
nil
|
109
|
+
end
|
110
|
+
|
95
111
|
# Run the SQL to drop the enum type with the given name.
|
96
112
|
# Options:
|
97
113
|
# :if_exists :: Do not raise an error if the enum type does not exist
|
@@ -109,15 +125,15 @@ module Sequel
|
|
109
125
|
# the pg_type table to get names and array oids for
|
110
126
|
# enums.
|
111
127
|
def parse_enum_labels
|
112
|
-
|
128
|
+
enum_labels = metadata_dataset.from(:pg_enum).
|
113
129
|
order(:enumtypid, :enumsortorder).
|
114
130
|
select_hash_groups(Sequel.cast(:enumtypid, Integer).as(:v), :enumlabel).freeze
|
115
|
-
|
131
|
+
enum_labels.each_value(&:freeze)
|
116
132
|
|
117
133
|
if respond_to?(:register_array_type)
|
118
134
|
array_types = metadata_dataset.
|
119
135
|
from(:pg_type).
|
120
|
-
where(:oid
|
136
|
+
where(:oid=>enum_labels.keys).
|
121
137
|
exclude(:typarray=>0).
|
122
138
|
select_map([:typname, Sequel.cast(:typarray, Integer).as(:v)])
|
123
139
|
|
@@ -127,13 +143,16 @@ module Sequel
|
|
127
143
|
register_array_type(name, :oid=>oid)
|
128
144
|
end
|
129
145
|
end
|
146
|
+
|
147
|
+
Sequel.synchronize{@enum_labels.replace(enum_labels)}
|
130
148
|
end
|
131
149
|
|
132
150
|
# For schema entries that are enums, set the type to
|
133
151
|
# :enum and add a :enum_values entry with the enum values.
|
134
152
|
def schema_post_process(_)
|
135
153
|
super.each do |_, s|
|
136
|
-
|
154
|
+
oid = s[:oid]
|
155
|
+
if values = Sequel.synchronize{@enum_labels[oid]}
|
137
156
|
s[:type] = :enum
|
138
157
|
s[:enum_values] = values
|
139
158
|
end
|
@@ -154,6 +173,10 @@ module Sequel
|
|
154
173
|
def create_enum(name, _)
|
155
174
|
@actions << [:drop_enum, name]
|
156
175
|
end
|
176
|
+
|
177
|
+
def rename_enum(old_name, new_name)
|
178
|
+
@actions << [:rename_enum, new_name, old_name]
|
179
|
+
end
|
157
180
|
end
|
158
181
|
end
|
159
182
|
|
@@ -61,11 +61,23 @@ module Sequel
|
|
61
61
|
# associated objects and call the method on the associated object array.
|
62
62
|
# Calling any other method will call that method on the association's dataset.
|
63
63
|
class AssociationProxy < BasicObject
|
64
|
-
array = []
|
64
|
+
array = [].freeze
|
65
65
|
|
66
|
-
|
67
|
-
|
68
|
-
|
66
|
+
if RUBY_VERSION < '2.6'
|
67
|
+
# Default proc used to determine whether to send the method to the dataset.
|
68
|
+
# If the array would respond to it, sends it to the array instead of the dataset.
|
69
|
+
DEFAULT_PROXY_TO_DATASET = proc do |opts|
|
70
|
+
array_method = array.respond_to?(opts[:method])
|
71
|
+
if !array_method && opts[:method] == :filter
|
72
|
+
Sequel::Deprecation.deprecate "The behavior of the #filter method for association proxies will change in Ruby 2.6. Switch from using #filter to using #where to conserve current behavior."
|
73
|
+
end
|
74
|
+
!array_method
|
75
|
+
end
|
76
|
+
else
|
77
|
+
# :nocov:
|
78
|
+
DEFAULT_PROXY_TO_DATASET = proc{|opts| !array.respond_to?(opts[:method])}
|
79
|
+
# :nocov:
|
80
|
+
end
|
69
81
|
|
70
82
|
# Set the association reflection to use, and whether the association should be
|
71
83
|
# reloaded if an array method is called.
|
@@ -32,25 +32,30 @@ module Sequel
|
|
32
32
|
# Album.plugin :error_splitter
|
33
33
|
module ErrorSplitter
|
34
34
|
module InstanceMethods
|
35
|
-
|
36
|
-
|
37
|
-
#
|
35
|
+
private
|
36
|
+
|
37
|
+
# If the model instance is not valid, split the errors before returning.
|
38
38
|
def _valid?(opts)
|
39
39
|
v = super
|
40
40
|
unless v
|
41
|
-
errors
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
41
|
+
split_validation_errors(errors)
|
42
|
+
end
|
43
|
+
v
|
44
|
+
end
|
45
|
+
|
46
|
+
# Go through all of the errors entries. For any that apply to multiple columns,
|
47
|
+
# remove them and add separate error entries, one per column.
|
48
|
+
def split_validation_errors(errors)
|
49
|
+
errors.keys.select{|k| k.is_a?(Array)}.each do |ks|
|
50
|
+
msgs = errors.delete(ks)
|
51
|
+
ks.each do |k|
|
52
|
+
msgs.each do |msg|
|
53
|
+
errors.add(k, msg)
|
47
54
|
end
|
48
55
|
end
|
49
56
|
end
|
50
|
-
v
|
51
57
|
end
|
52
58
|
end
|
53
59
|
end
|
54
60
|
end
|
55
61
|
end
|
56
|
-
|
@@ -0,0 +1,260 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
module Sequel
|
4
|
+
module Plugins
|
5
|
+
# The pg_auto_constraint_validations plugin automatically converts some constraint
|
6
|
+
# violation exceptions that are raised by INSERT/UPDATE queries into validation
|
7
|
+
# failures. This can allow for using the same error handling code for both
|
8
|
+
# regular validation errors (checked before attempting the INSERT/UPDATE), and
|
9
|
+
# constraint violations (raised during the INSERT/UPDATE).
|
10
|
+
#
|
11
|
+
# This handles the following constraint violations:
|
12
|
+
#
|
13
|
+
# * NOT NULL
|
14
|
+
# * CHECK
|
15
|
+
# * UNIQUE (except expression/functional indexes)
|
16
|
+
# * FOREIGN KEY (both referencing and referenced by)
|
17
|
+
#
|
18
|
+
# If the plugin cannot convert the constraint violation error to a validation
|
19
|
+
# error, it just reraises the initial exception, so this should not cause
|
20
|
+
# problems if the plugin doesn't know how to convert the exception.
|
21
|
+
#
|
22
|
+
# This plugin is not intended as a replacement for other validations,
|
23
|
+
# it is intended as a last resort. The purpose of validations is to provide nice
|
24
|
+
# error messages for the user, and the error messages generated by this plugin are
|
25
|
+
# fairly generic. The error messages can be customized using the :messages plugin
|
26
|
+
# option, but there is only a single message used per constraint type.
|
27
|
+
#
|
28
|
+
# This plugin only works on the postgres adapter when using the pg 0.16+ driver,
|
29
|
+
# PostgreSQL 9.3+ server, and PostgreSQL 9.3+ client library (libpq). In other cases
|
30
|
+
# it will be a no-op.
|
31
|
+
#
|
32
|
+
# Example:
|
33
|
+
#
|
34
|
+
# album = Album.new(:artist_id=>1) # Assume no such artist exists
|
35
|
+
# begin
|
36
|
+
# album.save
|
37
|
+
# rescue Sequel::ValidationFailed
|
38
|
+
# album.errors.on(:artist_id) # ['is invalid']
|
39
|
+
# end
|
40
|
+
#
|
41
|
+
# Usage:
|
42
|
+
#
|
43
|
+
# # Make all model subclasses automatically convert constraint violations
|
44
|
+
# # to validation failures (called before loading subclasses)
|
45
|
+
# Sequel::Model.plugin :pg_auto_constraint_validations
|
46
|
+
#
|
47
|
+
# # Make the Album class automatically convert constraint violations
|
48
|
+
# # to validation failures
|
49
|
+
# Album.plugin :pg_auto_constraint_validations
|
50
|
+
module PgAutoConstraintValidations
|
51
|
+
(
|
52
|
+
# The default error messages for each constraint violation type.
|
53
|
+
DEFAULT_ERROR_MESSAGES = {
|
54
|
+
:not_null=>"is not present",
|
55
|
+
:check=>"is invalid",
|
56
|
+
:unique=>'is already taken',
|
57
|
+
:foreign_key=>'is invalid',
|
58
|
+
:referenced_by=>'cannot be changed currently'
|
59
|
+
}.freeze).each_value(&:freeze)
|
60
|
+
|
61
|
+
# Setup the constraint violation metadata. Options:
|
62
|
+
# :messages :: Override the default error messages for each constraint
|
63
|
+
# violation type (:not_null, :check, :unique, :foreign_key, :referenced_by)
|
64
|
+
def self.configure(model, opts=OPTS)
|
65
|
+
model.instance_exec do
|
66
|
+
setup_pg_auto_constraint_validations
|
67
|
+
@pg_auto_constraint_validations_messages = (@pg_auto_constraint_validations_messages || DEFAULT_ERROR_MESSAGES).merge(opts[:messages] || {}).freeze
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
module ClassMethods
|
72
|
+
# Hash of metadata checked when an instance attempts to convert a constraint
|
73
|
+
# violation into a validation failure.
|
74
|
+
attr_reader :pg_auto_constraint_validations
|
75
|
+
|
76
|
+
# Hash of error messages keyed by constraint type symbol to use in the
|
77
|
+
# generated validation failures.
|
78
|
+
attr_reader :pg_auto_constraint_validations_messages
|
79
|
+
|
80
|
+
Plugins.inherited_instance_variables(self, :@pg_auto_constraint_validations=>nil, :@pg_auto_constraint_validations_messages=>nil)
|
81
|
+
Plugins.after_set_dataset(self, :setup_pg_auto_constraint_validations)
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
# Get the list of constraints, unique indexes, foreign keys in the current
|
86
|
+
# table, and keys in the current table referenced by foreign keys in other
|
87
|
+
# tables. Store this information so that if a constraint violation occurs,
|
88
|
+
# all necessary metadata is already available in the model, so a query is
|
89
|
+
# not required at runtime. This is both for performance and because in
|
90
|
+
# general after the constraint violation failure you will be inside a
|
91
|
+
# failed transaction and not able to execute queries.
|
92
|
+
def setup_pg_auto_constraint_validations
|
93
|
+
return unless @dataset
|
94
|
+
|
95
|
+
case @dataset.first_source_table
|
96
|
+
when Symbol, String, SQL::Identifier, SQL::QualifiedIdentifier
|
97
|
+
convert_errors = db.respond_to?(:error_info)
|
98
|
+
end
|
99
|
+
|
100
|
+
unless convert_errors
|
101
|
+
# Might be a table returning function or subquery, skip handling those.
|
102
|
+
# Might have db not support error_info, skip handling that.
|
103
|
+
@pg_auto_constraint_validations = nil
|
104
|
+
return
|
105
|
+
end
|
106
|
+
|
107
|
+
checks = {}
|
108
|
+
indexes = {}
|
109
|
+
foreign_keys = {}
|
110
|
+
referenced_by = {}
|
111
|
+
|
112
|
+
db.check_constraints(table_name).each do |k, v|
|
113
|
+
checks[k] = v[:columns].dup.freeze
|
114
|
+
end
|
115
|
+
db.indexes(table_name, :include_partial=>true).each do |k, v|
|
116
|
+
if v[:unique]
|
117
|
+
indexes[k] = v[:columns].dup.freeze
|
118
|
+
end
|
119
|
+
end
|
120
|
+
db.foreign_key_list(table_name, :schema=>false).each do |fk|
|
121
|
+
foreign_keys[fk[:name]] = fk[:columns].dup.freeze
|
122
|
+
end
|
123
|
+
db.foreign_key_list(table_name, :reverse=>true, :schema=>false).each do |fk|
|
124
|
+
referenced_by[[fk[:schema], fk[:table], fk[:name]].freeze] = fk[:key].dup.freeze
|
125
|
+
end
|
126
|
+
|
127
|
+
schema, table = db[:pg_class].
|
128
|
+
join(:pg_namespace, :oid=>:relnamespace, db.send(:regclass_oid, table_name)=>:oid).
|
129
|
+
get([:nspname, :relname])
|
130
|
+
|
131
|
+
(@pg_auto_constraint_validations = {
|
132
|
+
:schema=>schema,
|
133
|
+
:table=>table,
|
134
|
+
:check=>checks,
|
135
|
+
:unique=>indexes,
|
136
|
+
:foreign_key=>foreign_keys,
|
137
|
+
:referenced_by=>referenced_by
|
138
|
+
}.freeze).each_value(&:freeze)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
module InstanceMethods
|
143
|
+
private
|
144
|
+
|
145
|
+
# Yield to the given block, and if a Sequel::ConstraintViolation is raised, try
|
146
|
+
# to convert it to a Sequel::ValidationFailed error using the PostgreSQL error
|
147
|
+
# metadata.
|
148
|
+
def check_pg_constraint_error(ds)
|
149
|
+
yield
|
150
|
+
rescue Sequel::ConstraintViolation => e
|
151
|
+
begin
|
152
|
+
unless cv_info = model.pg_auto_constraint_validations
|
153
|
+
# Necessary metadata does not exist, just reraise the exception.
|
154
|
+
raise e
|
155
|
+
end
|
156
|
+
|
157
|
+
info = ds.db.error_info(e)
|
158
|
+
m = ds.method(:output_identifier)
|
159
|
+
schema = info[:schema]
|
160
|
+
table = info[:table]
|
161
|
+
if constraint = info[:constraint]
|
162
|
+
constraint = m.call(constraint)
|
163
|
+
end
|
164
|
+
messages = model.pg_auto_constraint_validations_messages
|
165
|
+
|
166
|
+
case e
|
167
|
+
when Sequel::NotNullConstraintViolation
|
168
|
+
if column = info[:column]
|
169
|
+
add_pg_constraint_validation_error([m.call(column)], messages[:not_null])
|
170
|
+
end
|
171
|
+
when Sequel::CheckConstraintViolation
|
172
|
+
if columns = cv_info[:check][constraint]
|
173
|
+
add_pg_constraint_validation_error(columns, messages[:check])
|
174
|
+
end
|
175
|
+
when Sequel::UniqueConstraintViolation
|
176
|
+
if columns = cv_info[:unique][constraint]
|
177
|
+
add_pg_constraint_validation_error(columns, messages[:unique])
|
178
|
+
end
|
179
|
+
when Sequel::ForeignKeyConstraintViolation
|
180
|
+
message_primary = info[:message_primary]
|
181
|
+
if message_primary.start_with?('update')
|
182
|
+
# This constraint violation is different from the others, because the constraint
|
183
|
+
# referenced is a constraint for a different table, not for this table. This
|
184
|
+
# happens when another table references the current table, and the referenced
|
185
|
+
# column in the current update is modified such that referential integrity
|
186
|
+
# would be broken. Use the reverse foreign key information to figure out
|
187
|
+
# which column is affected in that case.
|
188
|
+
skip_schema_table_check = true
|
189
|
+
if columns = cv_info[:referenced_by][[m.call(schema), m.call(table), constraint]]
|
190
|
+
add_pg_constraint_validation_error(columns, messages[:referenced_by])
|
191
|
+
end
|
192
|
+
elsif message_primary.start_with?('insert')
|
193
|
+
if columns = cv_info[:foreign_key][constraint]
|
194
|
+
add_pg_constraint_validation_error(columns, messages[:foreign_key])
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
rescue => e2
|
199
|
+
# If there is an error trying to conver the constraint violation
|
200
|
+
# into a validation failure, it's best to just raise the constraint
|
201
|
+
# violation. This can make debugging the above block of code more
|
202
|
+
# difficult.
|
203
|
+
raise e
|
204
|
+
else
|
205
|
+
unless skip_schema_table_check
|
206
|
+
# The constraint violation could be caused by a trigger modifying
|
207
|
+
# a different table. Check that the error schema and table
|
208
|
+
# match the model's schema and table, or clear the validation error
|
209
|
+
# that was set above.
|
210
|
+
if schema != cv_info[:schema] || table != cv_info[:table]
|
211
|
+
errors.clear
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
if errors.empty?
|
216
|
+
# If we weren't able to parse the constraint violation metadata and
|
217
|
+
# convert it to an appropriate validation failure, or the schema/table
|
218
|
+
# didn't match, then raise the constraint violation.
|
219
|
+
raise e
|
220
|
+
end
|
221
|
+
|
222
|
+
# Integrate with error_splitter plugin to split any multi-column errors
|
223
|
+
# and add them as separate single column errors
|
224
|
+
if respond_to?(:split_validation_errors, true)
|
225
|
+
split_validation_errors(errors)
|
226
|
+
end
|
227
|
+
|
228
|
+
vf = ValidationFailed.new(self)
|
229
|
+
vf.set_backtrace(e.backtrace)
|
230
|
+
vf.wrapped_exception = e
|
231
|
+
raise vf
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
# If there is a single column instead of an array of columns, add the error
|
236
|
+
# for the column, otherwise add the error for the array of columns.
|
237
|
+
def add_pg_constraint_validation_error(column, message)
|
238
|
+
column = column.first if column.length == 1
|
239
|
+
errors.add(column, message)
|
240
|
+
end
|
241
|
+
|
242
|
+
# Convert PostgreSQL constraint errors when inserting.
|
243
|
+
def _insert_raw(ds)
|
244
|
+
check_pg_constraint_error(ds){super}
|
245
|
+
end
|
246
|
+
|
247
|
+
# Convert PostgreSQL constraint errors when inserting.
|
248
|
+
def _insert_select_raw(ds)
|
249
|
+
check_pg_constraint_error(ds){super}
|
250
|
+
end
|
251
|
+
|
252
|
+
# Convert PostgreSQL constraint errors when updating.
|
253
|
+
def _update_without_checking(_)
|
254
|
+
check_pg_constraint_error(_update_dataset){super}
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|