activerecord-rescue_from_duplicate 0.0.6 → 0.0.7
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/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
|