activerecord-rescue_from_duplicate 0.0.6 → 0.0.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/rescue_from_duplicate/active_record.rb +22 -0
- data/lib/rescue_from_duplicate/active_record/extension.rb +17 -28
- data/lib/rescue_from_duplicate/active_record/version.rb +1 -1
- data/lib/rescue_from_duplicate/missing_unique_index.rb +11 -0
- data/lib/rescue_from_duplicate/rescuer.rb +3 -3
- data/lib/rescue_from_duplicate/uniqueness_rescuer.rb +23 -0
- data/spec/rescue_from_duplicate_spec.rb +1 -62
- data/spec/rescuer_spec.rb +10 -9
- data/spec/spec_helper.rb +6 -13
- data/spec/uniqueness_rescuer_spec.rb +34 -0
- data/spec/validator_spec.rb +41 -0
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 95ffd1c815f0296026de14cf92106766488a5472
|
4
|
+
data.tar.gz: 2f54109b6dbb3330942a64ce0644f95529e2431a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 76a7947095f99a17df0607fa139e45949ba4649f6bed2e9e1ee7efe2992863e697c9f0a285060c8003bc8c9282fb21478833859fd583f1fb02a8c54425a0dda4
|
7
|
+
data.tar.gz: 6e9d39085c3614245854a19023cdd4cd06d02ac8e20a7eee7f6ac2119b2b4fc78a92cef7f40f9b564b202af3cf4954c9b02f5bb1b68720e4aec0d664e721bf69
|
@@ -5,10 +5,32 @@ require "rescue_from_duplicate/active_record/version"
|
|
5
5
|
module RescueFromDuplicate
|
6
6
|
module ActiveRecord
|
7
7
|
end
|
8
|
+
|
9
|
+
def self.missing_unique_indexes
|
10
|
+
ar_subclasses = ObjectSpace.each_object(Class).select{ |klass| klass < ::ActiveRecord::Base }
|
11
|
+
klasses = ar_subclasses.select do |klass|
|
12
|
+
klass.validators.any? { |v| v.is_a?(::ActiveRecord::Validations::UniquenessValidator) || klass._rescue_from_duplicates.any? }
|
13
|
+
end
|
14
|
+
|
15
|
+
missing_unique_indexes = []
|
16
|
+
|
17
|
+
klasses.each do |klass|
|
18
|
+
klass._rescue_from_duplicate_handlers.each do |handler|
|
19
|
+
unique_indexes = klass.connection.indexes(klass.table_name).select(&:unique)
|
20
|
+
|
21
|
+
unless unique_indexes.any? { |index| index.columns.map(&:to_s).sort == handler.columns }
|
22
|
+
missing_unique_indexes << MissingUniqueIndex.new(klass, handler.attributes, handler.columns)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
missing_unique_indexes
|
27
|
+
end
|
8
28
|
end
|
9
29
|
|
10
30
|
require 'rescue_from_duplicate/active_record/extension'
|
31
|
+
require 'rescue_from_duplicate/uniqueness_rescuer'
|
11
32
|
require 'rescue_from_duplicate/rescuer'
|
33
|
+
require 'rescue_from_duplicate/missing_unique_index'
|
12
34
|
|
13
35
|
ActiveSupport.on_load(:active_record) do
|
14
36
|
::ActiveRecord::Base.send :include, RescueFromDuplicate::ActiveRecord::Extension
|
@@ -8,6 +8,13 @@ module RescueFromDuplicate::ActiveRecord
|
|
8
8
|
def rescue_from_duplicate(attribute, options = {})
|
9
9
|
self._rescue_from_duplicates += [RescueFromDuplicate::Rescuer.new(attribute, options)]
|
10
10
|
end
|
11
|
+
|
12
|
+
def _rescue_from_duplicate_handlers
|
13
|
+
validator_handlers = self.validators.select { |v| v.is_a?(ActiveRecord::Validations::UniquenessValidator) }.map do |v|
|
14
|
+
RescueFromDuplicate::UniquenessRescuer.new(v)
|
15
|
+
end
|
16
|
+
self._rescue_from_duplicates + validator_handlers
|
17
|
+
end
|
11
18
|
end
|
12
19
|
|
13
20
|
included do
|
@@ -18,7 +25,7 @@ module RescueFromDuplicate::ActiveRecord
|
|
18
25
|
def create_or_update(*params, &block)
|
19
26
|
super
|
20
27
|
rescue ActiveRecord::RecordNotUnique => exception
|
21
|
-
handler =
|
28
|
+
handler = exception_handler(exception)
|
22
29
|
|
23
30
|
raise exception unless handler
|
24
31
|
|
@@ -29,36 +36,25 @@ module RescueFromDuplicate::ActiveRecord
|
|
29
36
|
false
|
30
37
|
end
|
31
38
|
|
32
|
-
|
39
|
+
private
|
40
|
+
|
41
|
+
def exception_handler(exception)
|
33
42
|
columns = exception_columns(exception)
|
34
43
|
|
35
|
-
self.
|
36
|
-
|
37
|
-
next unless validator.is_a?(ActiveRecord::Validations::UniquenessValidator)
|
38
|
-
return validator if rescue_with_validator?(columns, validator)
|
39
|
-
end
|
44
|
+
self.class._rescue_from_duplicate_handlers.detect do |handler|
|
45
|
+
handler.rescue? && columns == handler.columns
|
40
46
|
end
|
41
|
-
|
42
|
-
nil
|
43
47
|
end
|
44
48
|
|
45
|
-
protected
|
46
|
-
|
47
49
|
def exception_columns(exception)
|
48
|
-
|
50
|
+
case
|
49
51
|
when exception.message =~ /SQLite3::ConstraintException/
|
50
52
|
sqlite3_exception_columns(exception)
|
51
53
|
when exception.message =~ /PG::UniqueViolation/
|
52
54
|
postgresql_exception_columns(exception)
|
53
55
|
else
|
54
56
|
other_exception_columns(exception)
|
55
|
-
end
|
56
|
-
end
|
57
|
-
|
58
|
-
def exception_rescuer(exception)
|
59
|
-
columns = exception_columns(exception)
|
60
|
-
|
61
|
-
_rescue_from_duplicates.detect { |rescuer| rescuer.matches?(columns) }
|
57
|
+
end.sort
|
62
58
|
end
|
63
59
|
|
64
60
|
def postgresql_exception_columns(exception)
|
@@ -71,19 +67,12 @@ module RescueFromDuplicate::ActiveRecord
|
|
71
67
|
|
72
68
|
def extract_columns(columns_string)
|
73
69
|
return unless columns_string
|
74
|
-
columns_string.split(",").map(&:strip)
|
70
|
+
columns_string.split(",").map(&:strip)
|
75
71
|
end
|
76
72
|
|
77
73
|
def other_exception_columns(exception)
|
78
74
|
indexes = self.class.connection.indexes(self.class.table_name)
|
79
|
-
|
80
|
-
columns.sort
|
81
|
-
end
|
82
|
-
|
83
|
-
def rescue_with_validator?(columns, validator)
|
84
|
-
validator_columns = (Array(validator.options[:scope]) + validator.attributes).map(&:to_s).sort
|
85
|
-
return false unless columns == validator_columns
|
86
|
-
validator.options.fetch(:rescue_from_duplicate) { false }
|
75
|
+
indexes.detect{ |i| exception.message.include?(i.name) }.try(:columns) || []
|
87
76
|
end
|
88
77
|
end
|
89
78
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
module RescueFromDuplicate
|
2
2
|
class Rescuer
|
3
|
-
attr_reader :attributes, :options
|
3
|
+
attr_reader :attributes, :options, :columns
|
4
4
|
|
5
5
|
def initialize(attribute, options)
|
6
6
|
@attributes = [attribute]
|
@@ -8,8 +8,8 @@ module RescueFromDuplicate
|
|
8
8
|
@options = options
|
9
9
|
end
|
10
10
|
|
11
|
-
def
|
12
|
-
|
11
|
+
def rescue?
|
12
|
+
true
|
13
13
|
end
|
14
14
|
end
|
15
15
|
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module RescueFromDuplicate
|
2
|
+
class UniquenessRescuer
|
3
|
+
def initialize(validator)
|
4
|
+
@validator = validator
|
5
|
+
end
|
6
|
+
|
7
|
+
def rescue?
|
8
|
+
@validator.options.fetch(:rescue_from_duplicate, false)
|
9
|
+
end
|
10
|
+
|
11
|
+
def options
|
12
|
+
@validator.options
|
13
|
+
end
|
14
|
+
|
15
|
+
def attributes
|
16
|
+
@validator.attributes
|
17
|
+
end
|
18
|
+
|
19
|
+
def columns
|
20
|
+
(Array(options[:scope]) + attributes).map(&:to_s).sort
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -10,67 +10,6 @@ shared_examples 'database error rescuing' do
|
|
10
10
|
allow(Rescuable).to receive(:connection).and_return(double(indexes: [Rescuable.index]))
|
11
11
|
end
|
12
12
|
|
13
|
-
describe "#exception_validator" do
|
14
|
-
context "validator can be found" do
|
15
|
-
it "returns the validator" do
|
16
|
-
expect(subject.exception_validator(uniqueness_exception)).to eq Rescuable.uniqueness_validator
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
20
|
-
context "validator cannot be found" do
|
21
|
-
before {
|
22
|
-
Rescuable.stub(_validators: {name: [Rescuable.presence_validator]})
|
23
|
-
}
|
24
|
-
|
25
|
-
it "returns nil" do
|
26
|
-
expect(subject.exception_validator(uniqueness_exception)).to be_nil
|
27
|
-
end
|
28
|
-
end
|
29
|
-
|
30
|
-
context "validator doesn't specify :rescue_from_duplicate" do
|
31
|
-
before {
|
32
|
-
Rescuable.stub(_validators: {name: [Rescuable.uniqueness_validator_without_rescue]})
|
33
|
-
}
|
34
|
-
|
35
|
-
it "returns nil" do
|
36
|
-
expect(subject.exception_validator(uniqueness_exception)).to be_nil
|
37
|
-
end
|
38
|
-
end
|
39
|
-
|
40
|
-
context "no validator" do
|
41
|
-
before {
|
42
|
-
Rescuable.stub(_validators: {})
|
43
|
-
}
|
44
|
-
|
45
|
-
it "returns nil" do
|
46
|
-
expect(subject.exception_validator(uniqueness_exception)).to be_nil
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
|
-
context "no index on the table" do
|
51
|
-
before {
|
52
|
-
Rescuable.stub(index: nil)
|
53
|
-
Rescuable.stub(connection: double(indexes: []))
|
54
|
-
}
|
55
|
-
|
56
|
-
let(:message) { super().gsub(/column (.*?) is/, 'column toto is').gsub(/Key \((.*?)\)=/, 'Key (toto)=') }
|
57
|
-
|
58
|
-
it "returns nil" do
|
59
|
-
expect(subject.exception_validator(uniqueness_exception)).to be_nil
|
60
|
-
end
|
61
|
-
end
|
62
|
-
|
63
|
-
context "columns part of the index of another table" do
|
64
|
-
before {
|
65
|
-
subject.stub(exception_columns: ['foo', 'baz'])
|
66
|
-
}
|
67
|
-
|
68
|
-
it "returns nil" do
|
69
|
-
expect(subject.exception_validator(uniqueness_exception)).to be_nil
|
70
|
-
end
|
71
|
-
end
|
72
|
-
end
|
73
|
-
|
74
13
|
describe "#create_or_update when the validation fails" do
|
75
14
|
before { Base.stub(exception: uniqueness_exception) }
|
76
15
|
|
@@ -82,7 +21,7 @@ shared_examples 'database error rescuing' do
|
|
82
21
|
end
|
83
22
|
|
84
23
|
context "when the validator is not present" do
|
85
|
-
before { Rescuable.stub(
|
24
|
+
before { Rescuable.stub(validators: [Rescuable.presence_validator]) }
|
86
25
|
|
87
26
|
it "raises an exception" do
|
88
27
|
expect{ subject.create_or_update }.to raise_error(ActiveRecord::RecordNotUnique)
|
data/spec/rescuer_spec.rb
CHANGED
@@ -1,17 +1,18 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
describe RescueFromDuplicate::Rescuer do
|
4
|
-
subject { RescueFromDuplicate::Rescuer.new(:
|
4
|
+
subject { RescueFromDuplicate::Rescuer.new(:name, scope: [:type, :shop_id], message: "Derp!") }
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
expect(subject.matches?(["name", "shop_id"])).to be true
|
10
|
-
end
|
6
|
+
it "always rescues" do
|
7
|
+
expect(subject.rescue?).to eq true
|
8
|
+
end
|
11
9
|
|
12
|
-
|
13
|
-
|
14
|
-
|
10
|
+
it "sorts the columns" do
|
11
|
+
expect(subject.columns).to eq ['name', 'shop_id', 'type']
|
12
|
+
end
|
13
|
+
|
14
|
+
it "returns the options" do
|
15
|
+
expect(subject.options).to eq scope: [:type, :shop_id], message: "Derp!"
|
15
16
|
end
|
16
17
|
end
|
17
18
|
|
data/spec/spec_helper.rb
CHANGED
@@ -43,28 +43,21 @@ module RescueFromDuplicate
|
|
43
43
|
send(attribute)
|
44
44
|
end
|
45
45
|
|
46
|
-
def _validators
|
47
|
-
self.class._validators
|
48
|
-
end
|
49
|
-
|
50
46
|
def errors
|
51
47
|
@errors ||= ActiveModel::Errors.new(self)
|
52
48
|
end
|
53
49
|
|
54
|
-
def self.
|
55
|
-
@validators ||=
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
presence_validator
|
60
|
-
]
|
61
|
-
}
|
50
|
+
def self.validators
|
51
|
+
@validators ||= [
|
52
|
+
uniqueness_validator,
|
53
|
+
presence_validator
|
54
|
+
]
|
62
55
|
end
|
63
56
|
|
64
57
|
def self.uniqueness_validator
|
65
58
|
@uniqueness_validator ||= ::ActiveRecord::Validations::UniquenessValidator.new(
|
66
59
|
attributes: [:name],
|
67
|
-
case_sensitive: true, scope: [:
|
60
|
+
case_sensitive: true, scope: [:type, :shop_id],
|
68
61
|
rescue_from_duplicate: true
|
69
62
|
).tap { |o| o.setup(self) if o.respond_to?(:setup) }
|
70
63
|
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe RescueFromDuplicate::UniquenessRescuer do
|
4
|
+
context "common" do
|
5
|
+
subject { RescueFromDuplicate::UniquenessRescuer.new(Rescuable.uniqueness_validator) }
|
6
|
+
|
7
|
+
it "returns the options" do
|
8
|
+
options = { case_sensitive: true, scope: [:type, :shop_id], rescue_from_duplicate: true }
|
9
|
+
expect(subject.options).to eq options
|
10
|
+
end
|
11
|
+
|
12
|
+
it "sorts the columns" do
|
13
|
+
expect(subject.columns).to eq ['name', 'shop_id', 'type']
|
14
|
+
end
|
15
|
+
|
16
|
+
it "returns the attributes" do
|
17
|
+
expect(subject.attributes).to eq [:name]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
context "validator with rescue" do
|
22
|
+
subject { RescueFromDuplicate::UniquenessRescuer.new(Rescuable.uniqueness_validator) }
|
23
|
+
it "rescues" do
|
24
|
+
expect(subject.rescue?).to eq true
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
context "validator without rescue" do
|
29
|
+
subject { RescueFromDuplicate::UniquenessRescuer.new(Rescuable.uniqueness_validator_without_rescue) }
|
30
|
+
it "doesn't rescues" do
|
31
|
+
expect(subject.rescue?).to eq false
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/spec/validator_spec.rb
CHANGED
@@ -45,18 +45,59 @@ shared_examples 'a model with rescued uniqueness validator' do
|
|
45
45
|
end
|
46
46
|
end
|
47
47
|
|
48
|
+
shared_examples 'missing index finding' do
|
49
|
+
describe do
|
50
|
+
context 'all indexes are satisfied' do
|
51
|
+
it 'returns an empty array' do
|
52
|
+
expect(RescueFromDuplicate.missing_unique_indexes).to be_empty
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
context 'indexes are missing' do
|
57
|
+
before {
|
58
|
+
described_class.stub(_rescue_from_duplicate_handlers: [
|
59
|
+
RescueFromDuplicate::UniquenessRescuer.new(
|
60
|
+
::ActiveRecord::Validations::UniquenessValidator.new(
|
61
|
+
attributes: [:name],
|
62
|
+
case_sensitive: true, scope: [:titi, :toto],
|
63
|
+
)
|
64
|
+
),
|
65
|
+
RescueFromDuplicate::Rescuer.new(:name, scope: [:hello])
|
66
|
+
])
|
67
|
+
}
|
68
|
+
|
69
|
+
it 'returns the missing indexes' do
|
70
|
+
missing_unique_indexes = RescueFromDuplicate.missing_unique_indexes.select { |mi| mi.model == described_class }
|
71
|
+
expect(missing_unique_indexes).not_to be_empty
|
72
|
+
|
73
|
+
expect(missing_unique_indexes.first.model).to eq described_class
|
74
|
+
expect(missing_unique_indexes.last.model).to eq described_class
|
75
|
+
|
76
|
+
expect(missing_unique_indexes.first.attributes).to eq [:name]
|
77
|
+
expect(missing_unique_indexes.last.attributes).to eq [:name]
|
78
|
+
|
79
|
+
expect(missing_unique_indexes.first.columns).to eq ["name", "titi", "toto"]
|
80
|
+
expect(missing_unique_indexes.last.columns).to eq ["hello", "name"]
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
48
86
|
describe Sqlite3Model do
|
49
87
|
it_behaves_like 'a model with rescued uniqueness validator'
|
88
|
+
it_behaves_like 'missing index finding'
|
50
89
|
end
|
51
90
|
|
52
91
|
if defined?(MysqlModel)
|
53
92
|
describe MysqlModel do
|
54
93
|
it_behaves_like 'a model with rescued uniqueness validator'
|
94
|
+
it_behaves_like 'missing index finding'
|
55
95
|
end
|
56
96
|
end
|
57
97
|
|
58
98
|
if defined?(PostgresqlModel)
|
59
99
|
describe PostgresqlModel do
|
60
100
|
it_behaves_like 'a model with rescued uniqueness validator'
|
101
|
+
it_behaves_like 'missing index finding'
|
61
102
|
end
|
62
103
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: activerecord-rescue_from_duplicate
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.7
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Guillaume Malette
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-10-
|
11
|
+
date: 2014-10-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -155,7 +155,9 @@ files:
|
|
155
155
|
- lib/rescue_from_duplicate/active_record.rb
|
156
156
|
- lib/rescue_from_duplicate/active_record/extension.rb
|
157
157
|
- lib/rescue_from_duplicate/active_record/version.rb
|
158
|
+
- lib/rescue_from_duplicate/missing_unique_index.rb
|
158
159
|
- lib/rescue_from_duplicate/rescuer.rb
|
160
|
+
- lib/rescue_from_duplicate/uniqueness_rescuer.rb
|
159
161
|
- shipit.rubygems.yml
|
160
162
|
- spec/gemfiles/Gemfile.ar-3.2
|
161
163
|
- spec/gemfiles/Gemfile.ar-4.0
|
@@ -165,6 +167,7 @@ files:
|
|
165
167
|
- spec/rescuer_spec.rb
|
166
168
|
- spec/spec_helper.rb
|
167
169
|
- spec/support/model.rb
|
170
|
+
- spec/uniqueness_rescuer_spec.rb
|
168
171
|
- spec/validator_spec.rb
|
169
172
|
homepage: https://github.com/Shopify/activerecord-rescue_from_duplicate
|
170
173
|
licenses:
|
@@ -200,4 +203,5 @@ test_files:
|
|
200
203
|
- spec/rescuer_spec.rb
|
201
204
|
- spec/spec_helper.rb
|
202
205
|
- spec/support/model.rb
|
206
|
+
- spec/uniqueness_rescuer_spec.rb
|
203
207
|
- spec/validator_spec.rb
|