activerecord-rescue_from_duplicate 0.0.2 → 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +3 -9
- data/README.md +40 -19
- data/Rakefile +1 -1
- data/activerecord-rescue_from_duplicate.gemspec +3 -4
- data/lib/rescue_from_duplicate/active_record.rb +1 -0
- data/lib/rescue_from_duplicate/active_record/extension.rb +47 -13
- data/lib/rescue_from_duplicate/active_record/version.rb +1 -1
- data/lib/rescue_from_duplicate/rescuer.rb +15 -0
- data/shipit.rubygems.yml +1 -0
- data/spec/gemfiles/Gemfile.ar-3.2 +1 -1
- data/spec/gemfiles/Gemfile.ar-4.0 +1 -1
- data/spec/gemfiles/Gemfile.ar-4.1 +10 -0
- data/spec/gemfiles/Gemfile.ar-edge +1 -1
- data/spec/rescue_from_duplicate_spec.rb +23 -27
- data/spec/rescuer_spec.rb +46 -0
- data/spec/spec_helper.rb +22 -20
- data/spec/support/model.rb +10 -4
- data/spec/validator_spec.rb +11 -11
- metadata +36 -44
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e499e51bfc3ec5a7d05bd7835a0cbb3916607905
|
4
|
+
data.tar.gz: d6c76963003fc3bac1b594f490680ad622251b3c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9dbc186269181a05853c12fed297986f0529dfb26bc3297a1758fc304da0c5966a634ed84044ccd0c67c08f4456e0f32d76938473845a45fe123982d3d54511c
|
7
|
+
data.tar.gz: 224278194bf55ade9ba082e1841513faa2b29ecbef1918887476dcb9bd126636e5d0c0e24a0ab9ec6b172ebc62d8f81b4040a0bc501fc96e85f29ba1d84d0fe3
|
data/.travis.yml
CHANGED
@@ -1,9 +1,11 @@
|
|
1
1
|
rvm:
|
2
2
|
- 1.9.3
|
3
3
|
- 2.0.0
|
4
|
+
- 2.1.2
|
4
5
|
gemfile:
|
5
6
|
- spec/gemfiles/Gemfile.ar-3.2
|
6
7
|
- spec/gemfiles/Gemfile.ar-4.0
|
8
|
+
- spec/gemfiles/Gemfile.ar-4.1
|
7
9
|
- spec/gemfiles/Gemfile.ar-edge
|
8
10
|
|
9
11
|
before_script:
|
@@ -11,16 +13,8 @@ before_script:
|
|
11
13
|
- psql -c 'create database rescue_from_duplicate;' -U postgres
|
12
14
|
|
13
15
|
env:
|
14
|
-
- POSTGRES=1
|
15
|
-
- MYSQL=1
|
16
|
+
- POSTGRES=1 MYSQL=1
|
16
17
|
|
17
18
|
matrix:
|
18
19
|
allow_failures:
|
19
20
|
- gemfile: gemfiles/Gemfile.ar-edge
|
20
|
-
- env: POSTGRES=1
|
21
|
-
- gemfile: gemfiles/Gemfile.ar-edge
|
22
|
-
env: MYSQL=1
|
23
|
-
rvm: 1.9.3
|
24
|
-
- gemfile: gemfiles/Gemfile.ar-edge
|
25
|
-
env: MYSQL=1
|
26
|
-
rvm: 2.0.0
|
data/README.md
CHANGED
@@ -2,53 +2,74 @@
|
|
2
2
|
|
3
3
|
[![Build Status](https://travis-ci.org/Shopify/activerecord-rescue_from_duplicate.png?branch=master)](https://travis-ci.org/Shopify/activerecord-rescue_from_duplicate)
|
4
4
|
|
5
|
-
This gem will rescue
|
6
|
-
PostgreSQL is not supported at the moment because of the errors raised when using prepared statements.
|
5
|
+
This gem will rescue SQL errors when trying to insert records that fail uniqueness validation.
|
7
6
|
|
8
7
|
It complements `:validates_uniqueness_of` and will add appropriate errors.
|
9
8
|
|
10
|
-
|
9
|
+
Additionally, a macro allows you to assume that the record will be unique and rescue gracefully otherwise.
|
11
10
|
|
12
|
-
|
13
|
-
* Unlike failed validation, `ActiveRecord::RecordNotSaved` will be raised when using `create!`, `save!` or other `!` methods.
|
11
|
+
Tested with:
|
14
12
|
|
15
|
-
|
16
|
-
|
17
|
-
|
13
|
+
- ActiveRecord: 3.2, 4.0, 4.1, edge
|
14
|
+
- Ruby 1.9.3, 2.0.0, 2.1.2
|
15
|
+
- MySQL, PostgreSQL, Sqlite3
|
18
16
|
|
19
|
-
|
20
|
-
|
21
|
-
And then execute:
|
17
|
+
**Note:**
|
22
18
|
|
23
|
-
|
19
|
+
* `after_validation`, `before_save`, `before_create` will have been run. Make sure they don't have undesired side-effects.
|
20
|
+
* Unlike failed validation, `ActiveRecord::RecordNotSaved` will be raised when using `create!`, `save!` or other `!` methods.
|
24
21
|
|
25
|
-
|
22
|
+
## Usage
|
26
23
|
|
27
|
-
|
24
|
+
### With validation
|
28
25
|
|
29
|
-
|
26
|
+
Add the `rescue_from_duplicate: true` to any regular uniqueness validation.
|
30
27
|
|
31
|
-
|
28
|
+
This will use the Rails standard validation, performing a `SELECT` to ensure the record is valid. In the case of a race condition, an error will be added to the model, and no exception will be thrown.
|
32
29
|
|
33
30
|
```ruby
|
34
31
|
class ModelWithUniquenessValidator < ActiveRecord::Base
|
35
|
-
validates_uniqueness_of :name, :
|
32
|
+
validates_uniqueness_of :name, scope: :shop_id, rescue_from_duplicate: true
|
36
33
|
end
|
37
34
|
```
|
38
35
|
|
39
36
|
If two of this statement go in at the same time, and the original validation on uniqueness of name passes, the DBMS will raise an duplicate record error.
|
40
37
|
|
41
38
|
```ruby
|
42
|
-
a = ModelWithUniquenessValidator.create(:
|
39
|
+
a = ModelWithUniquenessValidator.create(name: "name")
|
43
40
|
|
44
41
|
# in a different thread, causing race condition
|
45
|
-
b = ModelWithUniquenessValidator.create(:
|
42
|
+
b = ModelWithUniquenessValidator.create(name: "name")
|
46
43
|
|
47
44
|
a.persisted? #=> true
|
48
45
|
b.persisted? #=> false
|
49
46
|
b.errors[:name] #=> ["has already been taken"]
|
50
47
|
```
|
51
48
|
|
49
|
+
### Without validation
|
50
|
+
|
51
|
+
You can use this if you don't need to check that the record is unique before attempting to insert it. It will not add any validation to the model, but it will add an error if the persistance fails.
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
class ModelWithUniqueToken < ActiveRecord::Base
|
55
|
+
rescue_from_duplicate :token, scope: :shop_id
|
56
|
+
end
|
57
|
+
```
|
58
|
+
|
59
|
+
## Installation
|
60
|
+
|
61
|
+
Add this line to your application's Gemfile:
|
62
|
+
|
63
|
+
gem 'activerecord-rescue_from_duplicate'
|
64
|
+
|
65
|
+
And then execute:
|
66
|
+
|
67
|
+
$ bundle
|
68
|
+
|
69
|
+
Or install it yourself as:
|
70
|
+
|
71
|
+
$ gem install activerecord-rescue_from_duplicate
|
72
|
+
|
52
73
|
## Contributing
|
53
74
|
|
54
75
|
1. Fork it
|
data/Rakefile
CHANGED
@@ -7,7 +7,7 @@ Gem::Specification.new do |spec|
|
|
7
7
|
spec.name = "activerecord-rescue_from_duplicate"
|
8
8
|
spec.version = Activerecord::RescueFromDuplicate::VERSION
|
9
9
|
spec.authors = ["Guillaume Malette"]
|
10
|
-
spec.email = ["guillaume@
|
10
|
+
spec.email = ["guillaume@shopify.com"]
|
11
11
|
spec.description = %q{Rescue from MySQL and Sqlite duplicate errors}
|
12
12
|
spec.summary = %q{Rescue from MySQL and Sqlite duplicate errors when trying to insert records that fail uniqueness validation}
|
13
13
|
spec.homepage = "https://github.com/Shopify/activerecord-rescue_from_duplicate"
|
@@ -22,11 +22,10 @@ Gem::Specification.new do |spec|
|
|
22
22
|
|
23
23
|
spec.add_development_dependency "bundler", "~> 1.3"
|
24
24
|
spec.add_development_dependency "rake"
|
25
|
-
spec.add_development_dependency '
|
25
|
+
spec.add_development_dependency 'simplecov'
|
26
26
|
spec.add_development_dependency 'sqlite3'
|
27
|
-
spec.add_development_dependency 'pg'
|
28
27
|
spec.add_development_dependency 'mysql2'
|
29
28
|
spec.add_development_dependency 'rspec'
|
29
|
+
spec.add_development_dependency 'pg'
|
30
30
|
spec.add_development_dependency 'pry'
|
31
|
-
spec.add_development_dependency 'pry-debugger'
|
32
31
|
end
|
@@ -8,6 +8,7 @@ module RescueFromDuplicate
|
|
8
8
|
end
|
9
9
|
|
10
10
|
require 'rescue_from_duplicate/active_record/extension'
|
11
|
+
require 'rescue_from_duplicate/rescuer'
|
11
12
|
|
12
13
|
ActiveSupport.on_load(:active_record) do
|
13
14
|
::ActiveRecord::Base.send :include, RescueFromDuplicate::ActiveRecord::Extension
|
@@ -2,51 +2,85 @@ require 'active_support/core_ext/class'
|
|
2
2
|
|
3
3
|
module RescueFromDuplicate::ActiveRecord
|
4
4
|
module Extension
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
def rescue_from_duplicate(attribute, options = {})
|
9
|
+
self._rescue_from_duplicates += [RescueFromDuplicate::Rescuer.new(attribute, options)]
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
included do
|
14
|
+
class_attribute :_rescue_from_duplicates
|
15
|
+
self._rescue_from_duplicates = []
|
16
|
+
end
|
17
|
+
|
5
18
|
def create_or_update(*params, &block)
|
6
19
|
super
|
7
20
|
rescue ActiveRecord::RecordNotUnique => exception
|
8
|
-
|
21
|
+
handler = exception_validator(exception) || exception_rescuer(exception)
|
9
22
|
|
10
|
-
raise exception unless
|
23
|
+
raise exception unless handler
|
11
24
|
|
12
|
-
attribute =
|
13
|
-
options =
|
25
|
+
attribute = handler.attributes.first
|
26
|
+
options = handler.options.except(:case_sensitive, :scope).merge(value: self.send(:read_attribute_for_validation, attribute))
|
14
27
|
|
15
28
|
self.errors.add(attribute, :taken, options)
|
16
29
|
false
|
17
30
|
end
|
18
31
|
|
19
32
|
def exception_validator(exception)
|
20
|
-
columns = exception_columns(exception)
|
33
|
+
columns = exception_columns(exception)
|
21
34
|
|
22
35
|
self._validators.each do |attribute, validators|
|
23
36
|
validators.each do |validator|
|
24
37
|
next unless validator.is_a?(ActiveRecord::Validations::UniquenessValidator)
|
25
|
-
return validator if rescue_with_validator(columns, validator)
|
38
|
+
return validator if rescue_with_validator?(columns, validator)
|
26
39
|
end
|
27
40
|
end
|
28
41
|
|
29
42
|
nil
|
30
43
|
end
|
31
44
|
|
45
|
+
protected
|
46
|
+
|
32
47
|
def exception_columns(exception)
|
33
|
-
|
48
|
+
columns = case
|
49
|
+
when exception.message =~ /SQLite3::ConstraintException/
|
50
|
+
sqlite3_exception_columns(exception)
|
51
|
+
when exception.message =~ /PG::UniqueViolation/
|
52
|
+
postgresql_exception_columns(exception)
|
53
|
+
else
|
54
|
+
other_exception_columns(exception)
|
55
|
+
end
|
34
56
|
end
|
35
57
|
|
36
|
-
|
58
|
+
def exception_rescuer(exception)
|
59
|
+
columns = exception_columns(exception)
|
60
|
+
|
61
|
+
_rescue_from_duplicates.detect { |rescuer| rescuer.matches?(columns) }
|
62
|
+
end
|
63
|
+
|
64
|
+
def postgresql_exception_columns(exception)
|
65
|
+
extract_columns(exception.message[/Key \((.*?)\)=\(.*?\) already exists./, 1])
|
66
|
+
end
|
37
67
|
|
38
68
|
def sqlite3_exception_columns(exception)
|
39
|
-
|
40
|
-
|
41
|
-
|
69
|
+
extract_columns(exception.message[/columns? (.*) (?:is|are) not unique/, 1])
|
70
|
+
end
|
71
|
+
|
72
|
+
def extract_columns(columns_string)
|
73
|
+
return unless columns_string
|
74
|
+
columns_string.split(",").map(&:strip).sort
|
42
75
|
end
|
43
76
|
|
44
77
|
def other_exception_columns(exception)
|
45
78
|
indexes = self.class.connection.indexes(self.class.table_name)
|
46
|
-
indexes.detect{ |i| exception.message.include?(i.name) }.try(:columns) || []
|
79
|
+
columns = indexes.detect{ |i| exception.message.include?(i.name) }.try(:columns) || []
|
80
|
+
columns.sort
|
47
81
|
end
|
48
82
|
|
49
|
-
def rescue_with_validator(columns, validator)
|
83
|
+
def rescue_with_validator?(columns, validator)
|
50
84
|
validator_columns = (Array(validator.options[:scope]) + validator.attributes).map(&:to_s).sort
|
51
85
|
return false unless columns == validator_columns
|
52
86
|
validator.options.fetch(:rescue_from_duplicate) { false }
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module RescueFromDuplicate
|
2
|
+
class Rescuer
|
3
|
+
attr_reader :attributes, :options
|
4
|
+
|
5
|
+
def initialize(attribute, options)
|
6
|
+
@attributes = [attribute]
|
7
|
+
@columns = [attribute, *Array(options[:scope])].map(&:to_s)
|
8
|
+
@options = options
|
9
|
+
end
|
10
|
+
|
11
|
+
def matches?(columns)
|
12
|
+
@columns == columns.map(&:to_s).sort
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/shipit.rubygems.yml
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
# using the default shipit config
|
@@ -7,24 +7,7 @@ shared_examples 'database error rescuing' do
|
|
7
7
|
subject { Rescuable.new }
|
8
8
|
|
9
9
|
before do
|
10
|
-
Rescuable.
|
11
|
-
end
|
12
|
-
|
13
|
-
describe "#exception_columns" do
|
14
|
-
context "index cannot be found" do
|
15
|
-
let(:message) { super().gsub(/index_\w+/, "index_toto").gsub(/column .* is/, 'column toto is') }
|
16
|
-
let(:exception) { ActiveRecord::RecordNotUnique.new(message, nil) }
|
17
|
-
|
18
|
-
it "returns an array" do
|
19
|
-
expect(subject.exception_columns(exception)).to be_a Array
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
context "index can be found" do
|
24
|
-
it "returns the columns" do
|
25
|
-
expect(subject.exception_columns(uniqueness_exception).sort).to eq ["shop_id", "type", "name"].sort
|
26
|
-
end
|
27
|
-
end
|
10
|
+
allow(Rescuable).to receive(:connection).and_return(double(indexes: [Rescuable.index]))
|
28
11
|
end
|
29
12
|
|
30
13
|
describe "#exception_validator" do
|
@@ -36,7 +19,7 @@ shared_examples 'database error rescuing' do
|
|
36
19
|
|
37
20
|
context "validator cannot be found" do
|
38
21
|
before {
|
39
|
-
Rescuable.stub(:
|
22
|
+
Rescuable.stub(_validators: {name: [Rescuable.presence_validator]})
|
40
23
|
}
|
41
24
|
|
42
25
|
it "returns nil" do
|
@@ -46,7 +29,7 @@ shared_examples 'database error rescuing' do
|
|
46
29
|
|
47
30
|
context "validator doesn't specify :rescue_from_duplicate" do
|
48
31
|
before {
|
49
|
-
Rescuable.stub(:
|
32
|
+
Rescuable.stub(_validators: {name: [Rescuable.uniqueness_validator_without_rescue]})
|
50
33
|
}
|
51
34
|
|
52
35
|
it "returns nil" do
|
@@ -56,7 +39,7 @@ shared_examples 'database error rescuing' do
|
|
56
39
|
|
57
40
|
context "no validator" do
|
58
41
|
before {
|
59
|
-
Rescuable.stub(:
|
42
|
+
Rescuable.stub(_validators: {})
|
60
43
|
}
|
61
44
|
|
62
45
|
it "returns nil" do
|
@@ -66,11 +49,11 @@ shared_examples 'database error rescuing' do
|
|
66
49
|
|
67
50
|
context "no index on the table" do
|
68
51
|
before {
|
69
|
-
Rescuable.stub(:
|
70
|
-
Rescuable.stub(:
|
52
|
+
Rescuable.stub(index: nil)
|
53
|
+
Rescuable.stub(connection: double(indexes: []))
|
71
54
|
}
|
72
55
|
|
73
|
-
let(:message) { super().gsub(/column (
|
56
|
+
let(:message) { super().gsub(/column (.*?) is/, 'column toto is').gsub(/Key \((.*?)\)=/, 'Key (toto)=') }
|
74
57
|
|
75
58
|
it "returns nil" do
|
76
59
|
expect(subject.exception_validator(uniqueness_exception)).to be_nil
|
@@ -79,7 +62,7 @@ shared_examples 'database error rescuing' do
|
|
79
62
|
|
80
63
|
context "columns part of the index of another table" do
|
81
64
|
before {
|
82
|
-
subject.stub(:
|
65
|
+
subject.stub(exception_columns: ['foo', 'baz'])
|
83
66
|
}
|
84
67
|
|
85
68
|
it "returns nil" do
|
@@ -89,7 +72,7 @@ shared_examples 'database error rescuing' do
|
|
89
72
|
end
|
90
73
|
|
91
74
|
describe "#create_or_update when the validation fails" do
|
92
|
-
before { Base.stub(:
|
75
|
+
before { Base.stub(exception: uniqueness_exception) }
|
93
76
|
|
94
77
|
context "when the validator is present" do
|
95
78
|
it "adds an error to the model" do
|
@@ -99,13 +82,26 @@ shared_examples 'database error rescuing' do
|
|
99
82
|
end
|
100
83
|
|
101
84
|
context "when the validator is not present" do
|
102
|
-
before { Rescuable.stub(:
|
85
|
+
before { Rescuable.stub(_validators: {name: [Rescuable.presence_validator]}) }
|
103
86
|
|
104
87
|
it "raises an exception" do
|
105
88
|
expect{ subject.create_or_update }.to raise_error(ActiveRecord::RecordNotUnique)
|
106
89
|
end
|
107
90
|
end
|
108
91
|
end
|
92
|
+
|
93
|
+
describe "#create_or_update when using rescuer without validation" do
|
94
|
+
before {
|
95
|
+
Rescuable.stub(_validators: {})
|
96
|
+
Rescuable.stub(_rescue_from_duplicates: [Rescuable.uniqueness_rescuer])
|
97
|
+
Base.stub(exception: uniqueness_exception)
|
98
|
+
}
|
99
|
+
|
100
|
+
it "adds an error to the model" do
|
101
|
+
subject.create_or_update
|
102
|
+
expect(subject.errors[:name]).to eq ["is not unique by type and shop id"]
|
103
|
+
end
|
104
|
+
end
|
109
105
|
end
|
110
106
|
|
111
107
|
describe RescueFromDuplicate::ActiveRecord do
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe RescueFromDuplicate::Rescuer do
|
4
|
+
subject { RescueFromDuplicate::Rescuer.new(:name, scope: :shop_id) }
|
5
|
+
|
6
|
+
context "#matches?" do
|
7
|
+
it 'is true when the columns are the same' do
|
8
|
+
expect(subject.matches?(["shop_id", "name"])).to be true
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'is false when the columns are not the same' do
|
12
|
+
expect(subject.matches?(["shop_id", "toto"])).to be false
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
shared_examples 'a model with rescued unique error without validator' do
|
18
|
+
describe 'create!' do
|
19
|
+
context 'when catching a race condition' do
|
20
|
+
before {
|
21
|
+
described_class.create!(relation_id: 1, handle: 'toto')
|
22
|
+
}
|
23
|
+
|
24
|
+
it 'adds an error on the model' do
|
25
|
+
model = described_class.create(relation_id: 1, handle: 'toto')
|
26
|
+
expect(model.errors[:handle]).to eq(["handle must be unique for this relation"])
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe Sqlite3Model do
|
33
|
+
it_behaves_like 'a model with rescued unique error without validator'
|
34
|
+
end
|
35
|
+
|
36
|
+
if defined?(MysqlModel)
|
37
|
+
describe MysqlModel do
|
38
|
+
it_behaves_like 'a model with rescued unique error without validator'
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
if defined?(PostgresqlModel)
|
43
|
+
describe PostgresqlModel do
|
44
|
+
it_behaves_like 'a model with rescued unique error without validator'
|
45
|
+
end
|
46
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,23 +1,20 @@
|
|
1
|
+
require 'simplecov'
|
2
|
+
|
3
|
+
lib = File.expand_path('../lib', __FILE__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
|
6
|
+
SimpleCov.start do
|
7
|
+
add_filter '/spec/'
|
8
|
+
end
|
9
|
+
|
1
10
|
require 'active_record'
|
2
11
|
require 'activerecord-rescue_from_duplicate'
|
3
12
|
|
4
13
|
begin
|
5
14
|
require 'pry'
|
6
|
-
require 'pry-debugger'
|
7
15
|
rescue LoadError
|
8
16
|
end
|
9
17
|
|
10
|
-
require 'simplecov'
|
11
|
-
require 'coveralls'
|
12
|
-
|
13
|
-
# lib = File.expand_path('../lib', __FILE__)
|
14
|
-
# $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
15
|
-
|
16
|
-
# SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
|
17
|
-
# SimpleCov::Formatter::HTMLFormatter,
|
18
|
-
# Coveralls::SimpleCov::Formatter
|
19
|
-
# ]
|
20
|
-
# SimpleCov.start
|
21
18
|
|
22
19
|
|
23
20
|
module RescueFromDuplicate
|
@@ -56,7 +53,7 @@ module RescueFromDuplicate
|
|
56
53
|
|
57
54
|
def self._validators
|
58
55
|
@validators ||= {
|
59
|
-
:
|
56
|
+
name:
|
60
57
|
[
|
61
58
|
uniqueness_validator,
|
62
59
|
presence_validator
|
@@ -66,21 +63,27 @@ module RescueFromDuplicate
|
|
66
63
|
|
67
64
|
def self.uniqueness_validator
|
68
65
|
@uniqueness_validator ||= ::ActiveRecord::Validations::UniquenessValidator.new(
|
69
|
-
:
|
70
|
-
:
|
71
|
-
:
|
66
|
+
attributes: [:name],
|
67
|
+
case_sensitive: true, scope: [:shop_id, :type],
|
68
|
+
rescue_from_duplicate: true
|
72
69
|
).tap { |o| o.setup(self) if o.respond_to?(:setup) }
|
73
70
|
end
|
74
71
|
|
75
72
|
def self.uniqueness_validator_without_rescue
|
76
73
|
@uniqueness_validator_without_rescue ||= ::ActiveRecord::Validations::UniquenessValidator.new(
|
77
|
-
:
|
78
|
-
:
|
74
|
+
attributes: [:name],
|
75
|
+
case_sensitive: true, scope: [:shop_id, :type]
|
79
76
|
).tap { |o| o.setup(self) if o.respond_to?(:setup) }
|
80
77
|
end
|
81
78
|
|
79
|
+
def self.uniqueness_rescuer
|
80
|
+
@uniqueness_rescuer ||= RescueFromDuplicate::Rescuer.new(
|
81
|
+
:name, scope: [:shop_id, :type], message: "is not unique by type and shop id"
|
82
|
+
)
|
83
|
+
end
|
84
|
+
|
82
85
|
def self.presence_validator
|
83
|
-
@presence_validator ||= ActiveModel::Validations::PresenceValidator.new(:
|
86
|
+
@presence_validator ||= ActiveModel::Validations::PresenceValidator.new(attributes: [:name])
|
84
87
|
end
|
85
88
|
|
86
89
|
def self.index
|
@@ -107,7 +110,6 @@ Dir[File.expand_path(File.join(File.dirname(__FILE__), 'support', '**', '*.rb'))
|
|
107
110
|
#
|
108
111
|
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
109
112
|
RSpec.configure do |config|
|
110
|
-
config.treat_symbols_as_metadata_keys_with_true_values = true
|
111
113
|
config.run_all_when_everything_filtered = true
|
112
114
|
config.filter_run :focus
|
113
115
|
|
data/spec/support/model.rb
CHANGED
@@ -17,12 +17,16 @@ class CreateAllTables < ActiveRecord::Migration
|
|
17
17
|
execute "drop table if exists #{name}"
|
18
18
|
|
19
19
|
create_table(name, *args) do |t|
|
20
|
+
t.integer :relation_id
|
21
|
+
t.string :handle
|
22
|
+
|
20
23
|
t.string :name
|
21
24
|
t.integer :size
|
22
25
|
end
|
23
26
|
|
24
|
-
add_index name, :
|
25
|
-
add_index name, :
|
27
|
+
add_index name, [:relation_id, :handle], unique: true
|
28
|
+
add_index name, :name, unique: true
|
29
|
+
add_index name, :size, unique: true
|
26
30
|
end
|
27
31
|
|
28
32
|
def self.up
|
@@ -49,8 +53,10 @@ module TestModel
|
|
49
53
|
extend ActiveSupport::Concern
|
50
54
|
|
51
55
|
included do
|
52
|
-
|
53
|
-
|
56
|
+
rescue_from_duplicate :handle, scope: :relation_id, message: "handle must be unique for this relation"
|
57
|
+
|
58
|
+
validates_uniqueness_of :name, rescue_from_duplicate: true, allow_nil: true
|
59
|
+
validates_uniqueness_of :size, allow_nil: true
|
54
60
|
end
|
55
61
|
end
|
56
62
|
|
data/spec/validator_spec.rb
CHANGED
@@ -1,22 +1,22 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
-
shared_examples 'a model with rescued
|
3
|
+
shared_examples 'a model with rescued uniqueness validator' do
|
4
4
|
describe 'create!' do
|
5
5
|
context 'when catching a race condition' do
|
6
6
|
|
7
7
|
before(:each) {
|
8
|
-
ActiveRecord::Validations::UniquenessValidator.any_instance.stub(:
|
9
|
-
described_class.create!(:
|
8
|
+
ActiveRecord::Validations::UniquenessValidator.any_instance.stub(validate_each: nil)
|
9
|
+
described_class.create!(name: 'toto', size: 5)
|
10
10
|
}
|
11
11
|
|
12
12
|
it 'raises an ActiveRecord::RecordNotSaved error' do
|
13
|
-
expect{ described_class.create!(:
|
13
|
+
expect{ described_class.create!(name: 'toto') }.to raise_error(ActiveRecord::RecordNotSaved)
|
14
14
|
end
|
15
15
|
|
16
16
|
it "doesn't save the record" do
|
17
17
|
expect{
|
18
18
|
begin
|
19
|
-
described_class.create!(:
|
19
|
+
described_class.create!(name: 'toto')
|
20
20
|
rescue ActiveRecord::RecordNotSaved
|
21
21
|
# NOOP
|
22
22
|
end
|
@@ -27,8 +27,8 @@ shared_examples 'a model with rescued unique' do
|
|
27
27
|
expect {
|
28
28
|
begin
|
29
29
|
described_class.transaction do
|
30
|
-
described_class.create(:
|
31
|
-
described_class.create!(:
|
30
|
+
described_class.create(name: 'not toto', size: 55)
|
31
|
+
described_class.create!(name: 'toto')
|
32
32
|
end
|
33
33
|
rescue ActiveRecord::RecordNotSaved
|
34
34
|
# NOOP
|
@@ -39,24 +39,24 @@ shared_examples 'a model with rescued unique' do
|
|
39
39
|
|
40
40
|
context "with no race condition" do
|
41
41
|
it 'saves the model' do
|
42
|
-
expect{ described_class.create!(:
|
42
|
+
expect{ described_class.create!(name: 'toto') }.to change(described_class, :count).by(1)
|
43
43
|
end
|
44
44
|
end
|
45
45
|
end
|
46
46
|
end
|
47
47
|
|
48
48
|
describe Sqlite3Model do
|
49
|
-
it_behaves_like 'a model with rescued
|
49
|
+
it_behaves_like 'a model with rescued uniqueness validator'
|
50
50
|
end
|
51
51
|
|
52
52
|
if defined?(MysqlModel)
|
53
53
|
describe MysqlModel do
|
54
|
-
it_behaves_like 'a model with rescued
|
54
|
+
it_behaves_like 'a model with rescued uniqueness validator'
|
55
55
|
end
|
56
56
|
end
|
57
57
|
|
58
58
|
if defined?(PostgresqlModel)
|
59
59
|
describe PostgresqlModel do
|
60
|
-
it_behaves_like 'a model with rescued
|
60
|
+
it_behaves_like 'a model with rescued uniqueness validator'
|
61
61
|
end
|
62
62
|
end
|
metadata
CHANGED
@@ -1,165 +1,151 @@
|
|
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.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Guillaume Malette
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2014-09-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- -
|
17
|
+
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: '3.2'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- -
|
24
|
+
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '3.2'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: bundler
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- - ~>
|
31
|
+
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
33
|
version: '1.3'
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- - ~>
|
38
|
+
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '1.3'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: rake
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
|
-
- -
|
45
|
+
- - ">="
|
46
46
|
- !ruby/object:Gem::Version
|
47
47
|
version: '0'
|
48
48
|
type: :development
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
|
-
- -
|
52
|
+
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '0'
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
|
-
name:
|
56
|
+
name: simplecov
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
|
-
- -
|
59
|
+
- - ">="
|
60
60
|
- !ruby/object:Gem::Version
|
61
61
|
version: '0'
|
62
62
|
type: :development
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
|
-
- -
|
66
|
+
- - ">="
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '0'
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: sqlite3
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
72
72
|
requirements:
|
73
|
-
- -
|
73
|
+
- - ">="
|
74
74
|
- !ruby/object:Gem::Version
|
75
75
|
version: '0'
|
76
76
|
type: :development
|
77
77
|
prerelease: false
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
79
79
|
requirements:
|
80
|
-
- -
|
81
|
-
- !ruby/object:Gem::Version
|
82
|
-
version: '0'
|
83
|
-
- !ruby/object:Gem::Dependency
|
84
|
-
name: pg
|
85
|
-
requirement: !ruby/object:Gem::Requirement
|
86
|
-
requirements:
|
87
|
-
- - '>='
|
88
|
-
- !ruby/object:Gem::Version
|
89
|
-
version: '0'
|
90
|
-
type: :development
|
91
|
-
prerelease: false
|
92
|
-
version_requirements: !ruby/object:Gem::Requirement
|
93
|
-
requirements:
|
94
|
-
- - '>='
|
80
|
+
- - ">="
|
95
81
|
- !ruby/object:Gem::Version
|
96
82
|
version: '0'
|
97
83
|
- !ruby/object:Gem::Dependency
|
98
84
|
name: mysql2
|
99
85
|
requirement: !ruby/object:Gem::Requirement
|
100
86
|
requirements:
|
101
|
-
- -
|
87
|
+
- - ">="
|
102
88
|
- !ruby/object:Gem::Version
|
103
89
|
version: '0'
|
104
90
|
type: :development
|
105
91
|
prerelease: false
|
106
92
|
version_requirements: !ruby/object:Gem::Requirement
|
107
93
|
requirements:
|
108
|
-
- -
|
94
|
+
- - ">="
|
109
95
|
- !ruby/object:Gem::Version
|
110
96
|
version: '0'
|
111
97
|
- !ruby/object:Gem::Dependency
|
112
98
|
name: rspec
|
113
99
|
requirement: !ruby/object:Gem::Requirement
|
114
100
|
requirements:
|
115
|
-
- -
|
101
|
+
- - ">="
|
116
102
|
- !ruby/object:Gem::Version
|
117
103
|
version: '0'
|
118
104
|
type: :development
|
119
105
|
prerelease: false
|
120
106
|
version_requirements: !ruby/object:Gem::Requirement
|
121
107
|
requirements:
|
122
|
-
- -
|
108
|
+
- - ">="
|
123
109
|
- !ruby/object:Gem::Version
|
124
110
|
version: '0'
|
125
111
|
- !ruby/object:Gem::Dependency
|
126
|
-
name:
|
112
|
+
name: pg
|
127
113
|
requirement: !ruby/object:Gem::Requirement
|
128
114
|
requirements:
|
129
|
-
- -
|
115
|
+
- - ">="
|
130
116
|
- !ruby/object:Gem::Version
|
131
117
|
version: '0'
|
132
118
|
type: :development
|
133
119
|
prerelease: false
|
134
120
|
version_requirements: !ruby/object:Gem::Requirement
|
135
121
|
requirements:
|
136
|
-
- -
|
122
|
+
- - ">="
|
137
123
|
- !ruby/object:Gem::Version
|
138
124
|
version: '0'
|
139
125
|
- !ruby/object:Gem::Dependency
|
140
|
-
name: pry
|
126
|
+
name: pry
|
141
127
|
requirement: !ruby/object:Gem::Requirement
|
142
128
|
requirements:
|
143
|
-
- -
|
129
|
+
- - ">="
|
144
130
|
- !ruby/object:Gem::Version
|
145
131
|
version: '0'
|
146
132
|
type: :development
|
147
133
|
prerelease: false
|
148
134
|
version_requirements: !ruby/object:Gem::Requirement
|
149
135
|
requirements:
|
150
|
-
- -
|
136
|
+
- - ">="
|
151
137
|
- !ruby/object:Gem::Version
|
152
138
|
version: '0'
|
153
139
|
description: Rescue from MySQL and Sqlite duplicate errors
|
154
140
|
email:
|
155
|
-
- guillaume@
|
141
|
+
- guillaume@shopify.com
|
156
142
|
executables: []
|
157
143
|
extensions: []
|
158
144
|
extra_rdoc_files: []
|
159
145
|
files:
|
160
|
-
- .gitignore
|
161
|
-
- .rspec
|
162
|
-
- .travis.yml
|
146
|
+
- ".gitignore"
|
147
|
+
- ".rspec"
|
148
|
+
- ".travis.yml"
|
163
149
|
- Gemfile
|
164
150
|
- LICENSE.txt
|
165
151
|
- README.md
|
@@ -169,10 +155,14 @@ files:
|
|
169
155
|
- lib/rescue_from_duplicate/active_record.rb
|
170
156
|
- lib/rescue_from_duplicate/active_record/extension.rb
|
171
157
|
- lib/rescue_from_duplicate/active_record/version.rb
|
158
|
+
- lib/rescue_from_duplicate/rescuer.rb
|
159
|
+
- shipit.rubygems.yml
|
172
160
|
- spec/gemfiles/Gemfile.ar-3.2
|
173
161
|
- spec/gemfiles/Gemfile.ar-4.0
|
162
|
+
- spec/gemfiles/Gemfile.ar-4.1
|
174
163
|
- spec/gemfiles/Gemfile.ar-edge
|
175
164
|
- spec/rescue_from_duplicate_spec.rb
|
165
|
+
- spec/rescuer_spec.rb
|
176
166
|
- spec/spec_helper.rb
|
177
167
|
- spec/support/model.rb
|
178
168
|
- spec/validator_spec.rb
|
@@ -186,17 +176,17 @@ require_paths:
|
|
186
176
|
- lib
|
187
177
|
required_ruby_version: !ruby/object:Gem::Requirement
|
188
178
|
requirements:
|
189
|
-
- -
|
179
|
+
- - ">="
|
190
180
|
- !ruby/object:Gem::Version
|
191
181
|
version: '0'
|
192
182
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
193
183
|
requirements:
|
194
|
-
- -
|
184
|
+
- - ">="
|
195
185
|
- !ruby/object:Gem::Version
|
196
186
|
version: '0'
|
197
187
|
requirements: []
|
198
188
|
rubyforge_project:
|
199
|
-
rubygems_version: 2.
|
189
|
+
rubygems_version: 2.2.2
|
200
190
|
signing_key:
|
201
191
|
specification_version: 4
|
202
192
|
summary: Rescue from MySQL and Sqlite duplicate errors when trying to insert records
|
@@ -204,8 +194,10 @@ summary: Rescue from MySQL and Sqlite duplicate errors when trying to insert rec
|
|
204
194
|
test_files:
|
205
195
|
- spec/gemfiles/Gemfile.ar-3.2
|
206
196
|
- spec/gemfiles/Gemfile.ar-4.0
|
197
|
+
- spec/gemfiles/Gemfile.ar-4.1
|
207
198
|
- spec/gemfiles/Gemfile.ar-edge
|
208
199
|
- spec/rescue_from_duplicate_spec.rb
|
200
|
+
- spec/rescuer_spec.rb
|
209
201
|
- spec/spec_helper.rb
|
210
202
|
- spec/support/model.rb
|
211
203
|
- spec/validator_spec.rb
|