activerecord-rescue_from_duplicate 0.0.1
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 +15 -0
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/.travis.yml +26 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +56 -0
- data/Rakefile +20 -0
- data/activerecord-rescue_from_duplicate.gemspec +32 -0
- data/lib/activerecord-rescue_from_duplicate.rb +1 -0
- data/lib/rescue_from_duplicate/active_record.rb +14 -0
- data/lib/rescue_from_duplicate/active_record/extension.rb +55 -0
- data/lib/rescue_from_duplicate/active_record/version.rb +5 -0
- data/spec/gemfiles/Gemfile.ar-3.2 +10 -0
- data/spec/gemfiles/Gemfile.ar-4.0 +10 -0
- data/spec/gemfiles/Gemfile.ar-edge +12 -0
- data/spec/rescue_from_duplicate_spec.rb +97 -0
- data/spec/spec_helper.rb +119 -0
- data/spec/support/model.rb +90 -0
- data/spec/validator_spec.rb +62 -0
- metadata +212 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
ZDA5ZTEwOWE2NjIxMTQ5MWY0MDI0YTM2NDZmNzFlYTkwZDE4NGQzMw==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
NThhZTcxOTExYzkyZjZhNTYyYjFiZTJlZjRhODdmZWM2MjEwZDNmNg==
|
7
|
+
SHA512:
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
NDljN2M4MjcxZmUxMjhhNGRjNTgyZDk3YThkNTBkYzEzODc3MWNjMmNkMTE2
|
10
|
+
ZWI0ZDQ3YmMzN2EwMTUwNTVjNmEyYmE4MGY5ODQxODJhNDU5ZWJmYTZjYjU4
|
11
|
+
YjUwZDI0MjUwNjcyYTI0MTJmNjdmMWU0YzQ4ZWRiZDk2OTExNmU=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
MzRhMDQ1MmM2MTdmZGJiZTRmZGFmNGQ5ZjdhYWY3MDJkNGRkZjI2MzRiMTJi
|
14
|
+
NmEzMjJhYTIzYjA0MjQ4NWZmMDMyMzZhNThiZjgyMzFjNDIxOWI5MDY4MTU2
|
15
|
+
Mjg2NjhlZGUzOWQ2Y2EwYTY2MzQ2YzZmZTYzMTBiOGE0YjliOGE=
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
rvm:
|
2
|
+
- 1.9.3
|
3
|
+
- 2.0.0
|
4
|
+
gemfile:
|
5
|
+
- spec/gemfiles/Gemfile.ar-3.2
|
6
|
+
- spec/gemfiles/Gemfile.ar-4.0
|
7
|
+
- spec/gemfiles/Gemfile.ar-edge
|
8
|
+
|
9
|
+
before_script:
|
10
|
+
- mysql -e 'create database rescue_from_duplicate;'
|
11
|
+
- psql -c 'create database rescue_from_duplicate;' -U postgres
|
12
|
+
|
13
|
+
env:
|
14
|
+
- POSTGRES=1
|
15
|
+
- MYSQL=1
|
16
|
+
|
17
|
+
matrix:
|
18
|
+
allow_failures:
|
19
|
+
- 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/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Guillaume Malette
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
# ActiveRecord - RescueFromDuplicate
|
2
|
+
|
3
|
+
This gem will rescue from MySQL and Sqlite errors when trying to insert records that fail uniqueness validation.
|
4
|
+
PostgreSQL is not supported at the moment because of the errors raised when using prepared statements.
|
5
|
+
|
6
|
+
It complements `:validates_uniqueness_of` and will add appropriate errors.
|
7
|
+
|
8
|
+
**Note:**
|
9
|
+
|
10
|
+
* All `before_*` filters will have been run.
|
11
|
+
* Unlike failed validation, `ActiveRecord::RecordNotSaved` will be raised when using `create!`, `save!` or other `!` methods.
|
12
|
+
|
13
|
+
## Installation
|
14
|
+
|
15
|
+
Add this line to your application's Gemfile:
|
16
|
+
|
17
|
+
gem 'activerecord-rescue_from_duplicate'
|
18
|
+
|
19
|
+
And then execute:
|
20
|
+
|
21
|
+
$ bundle
|
22
|
+
|
23
|
+
Or install it yourself as:
|
24
|
+
|
25
|
+
$ gem install activerecord-rescue_from_duplicate
|
26
|
+
|
27
|
+
## Usage
|
28
|
+
|
29
|
+
Add the `:rescue_from_duplicate => true` to any regular uniqueness validation.
|
30
|
+
|
31
|
+
```ruby
|
32
|
+
class ModelWithUniquenessValidator < ActiveRecord::Base
|
33
|
+
validates_uniqueness_of :name, :scope => :shop_id, :rescue_from_duplicate => true
|
34
|
+
end
|
35
|
+
```
|
36
|
+
|
37
|
+
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.
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
a = ModelWithUniquenessValidator.create(:name => "name")
|
41
|
+
|
42
|
+
# in a different thread, causing race condition
|
43
|
+
b = ModelWithUniquenessValidator.create(:name => "name")
|
44
|
+
|
45
|
+
a.persisted? #=> true
|
46
|
+
b.persisted? #=> false
|
47
|
+
b.errors[:name] #=> ["has already been taken"]
|
48
|
+
```
|
49
|
+
|
50
|
+
## Contributing
|
51
|
+
|
52
|
+
1. Fork it
|
53
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
54
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
55
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
56
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require 'rspec/core/rake_task'
|
3
|
+
|
4
|
+
RSpec::Core::RakeTask.new(:spec)
|
5
|
+
|
6
|
+
task :default => :spec
|
7
|
+
|
8
|
+
namespace :spec do
|
9
|
+
task :all do
|
10
|
+
%w(3.2 4.0 edge).each do |ar_version|
|
11
|
+
system(
|
12
|
+
{
|
13
|
+
"BUNDLE_GEMFILE" => "spec/gemfiles/Gemfile.ar-#{ar_version}",
|
14
|
+
"MYSQL" => "1"
|
15
|
+
},
|
16
|
+
"rspec"
|
17
|
+
)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'rescue_from_duplicate/active_record/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "activerecord-rescue_from_duplicate"
|
8
|
+
spec.version = Activerecord::RescueFromDuplicate::VERSION
|
9
|
+
spec.authors = ["Guillaume Malette"]
|
10
|
+
spec.email = ["guillaume@jadedpixel.com"]
|
11
|
+
spec.description = %q{Rescue from MySQL and Sqlite duplicate errors}
|
12
|
+
spec.summary = %q{Rescue from MySQL and Sqlite duplicate errors when trying to insert records that fail uniqueness validation}
|
13
|
+
spec.homepage = "https://github.com/Shopify/activerecord-rescue_from_duplicate"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency 'activerecord', '>= 3.2'
|
22
|
+
|
23
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
24
|
+
spec.add_development_dependency "rake"
|
25
|
+
spec.add_development_dependency 'coveralls'
|
26
|
+
spec.add_development_dependency 'sqlite3'
|
27
|
+
spec.add_development_dependency 'pg'
|
28
|
+
spec.add_development_dependency 'mysql2'
|
29
|
+
spec.add_development_dependency 'rspec'
|
30
|
+
spec.add_development_dependency 'pry'
|
31
|
+
spec.add_development_dependency 'pry-debugger'
|
32
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'rescue_from_duplicate/active_record'
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
require 'active_support/core_ext/object/try'
|
3
|
+
require "rescue_from_duplicate/active_record/version"
|
4
|
+
|
5
|
+
module RescueFromDuplicate
|
6
|
+
module ActiveRecord
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
require 'rescue_from_duplicate/active_record/extension'
|
11
|
+
|
12
|
+
ActiveSupport.on_load(:active_record) do
|
13
|
+
::ActiveRecord::Base.send :include, RescueFromDuplicate::ActiveRecord::Extension
|
14
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'active_support/core_ext/class'
|
2
|
+
|
3
|
+
module RescueFromDuplicate::ActiveRecord
|
4
|
+
module Extension
|
5
|
+
def create_or_update(*params, &block)
|
6
|
+
super
|
7
|
+
rescue ActiveRecord::RecordNotUnique => e
|
8
|
+
validator = exception_validator(e)
|
9
|
+
|
10
|
+
raise e unless validator
|
11
|
+
|
12
|
+
attribute = validator.attributes.first
|
13
|
+
options = validator.options.except(:case_sensitive, :scope).merge(:value => self.send(:read_attribute_for_validation, attribute))
|
14
|
+
|
15
|
+
self.errors.add(attribute, :taken, options)
|
16
|
+
false
|
17
|
+
end
|
18
|
+
|
19
|
+
def exception_validator(exception)
|
20
|
+
columns = exception_columns(exception).sort
|
21
|
+
|
22
|
+
self._validators.each do |attribute, validators|
|
23
|
+
validators.each do |validator|
|
24
|
+
next unless validator.is_a?(ActiveRecord::Validations::UniquenessValidator)
|
25
|
+
return validator if rescue_with_validator(columns, validator)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
nil
|
30
|
+
end
|
31
|
+
|
32
|
+
def exception_columns(exception)
|
33
|
+
exception.message =~ /SQLite3::ConstraintException/ ? sqlite3_exception_columns(exception) : other_exception_columns(exception)
|
34
|
+
end
|
35
|
+
|
36
|
+
protected
|
37
|
+
|
38
|
+
def sqlite3_exception_columns(exception)
|
39
|
+
columns = exception.message[/column (.*) is not unique/, 1]
|
40
|
+
return unless columns
|
41
|
+
columns.split(",").map(&:strip)
|
42
|
+
end
|
43
|
+
|
44
|
+
def other_exception_columns(exception)
|
45
|
+
indexes = self.class.connection.indexes(self.class.table_name)
|
46
|
+
indexes.detect{ |i| exception.message.include?(i.name) }.try(:columns)
|
47
|
+
end
|
48
|
+
|
49
|
+
def rescue_with_validator(columns, validator)
|
50
|
+
validator_columns = (Array(validator.options[:scope]) + validator.attributes).map(&:to_s).sort
|
51
|
+
return false unless columns == validator_columns
|
52
|
+
validator.options.fetch(:rescue_from_duplicate) { false }
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
include RescueFromDuplicate
|
3
|
+
|
4
|
+
shared_examples 'database error rescuing' do
|
5
|
+
let(:uniqueness_exception) { ::ActiveRecord::RecordNotUnique.new(message, nil) }
|
6
|
+
|
7
|
+
subject { Rescuable.new }
|
8
|
+
|
9
|
+
before do
|
10
|
+
Rescuable.stub(:connection => double(:indexes => [Rescuable.index]))
|
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/, 'toto') }
|
16
|
+
let(:exception) { ActiveRecord::RecordNotUnique.new(message, nil) }
|
17
|
+
|
18
|
+
it "returns nil" do
|
19
|
+
expect(subject.exception_columns(exception)).to be_nil
|
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
|
28
|
+
end
|
29
|
+
|
30
|
+
describe "#exception_validator" do
|
31
|
+
context "validator can be found" do
|
32
|
+
it "returns the validator" do
|
33
|
+
expect(subject.exception_validator(uniqueness_exception)).to eq Rescuable.uniqueness_validator
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
context "validator cannot be found" do
|
38
|
+
before {
|
39
|
+
Rescuable.stub(:_validators => {:name => [Rescuable.presence_validator]})
|
40
|
+
}
|
41
|
+
|
42
|
+
it "returns nil" do
|
43
|
+
expect(subject.exception_validator(uniqueness_exception)).to be_nil
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
context "validator doesn't specify :rescue_from_duplicate" do
|
48
|
+
before {
|
49
|
+
Rescuable.stub(:_validators => {:name => [Rescuable.uniqueness_validator_without_rescue]})
|
50
|
+
}
|
51
|
+
|
52
|
+
it "returns nil" do
|
53
|
+
expect(subject.exception_validator(uniqueness_exception)).to be_nil
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe "#create_or_update when the validation fails" do
|
59
|
+
before { Base.stub(:exception => uniqueness_exception) }
|
60
|
+
|
61
|
+
context "when the validator is present" do
|
62
|
+
it "adds an error to the model" do
|
63
|
+
subject.create_or_update
|
64
|
+
expect(subject.errors[:name]).to eq ["has already been taken"]
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
context "when the validator is not present" do
|
69
|
+
before { Rescuable.stub(:_validators => {:name => [Rescuable.presence_validator]}) }
|
70
|
+
|
71
|
+
it "raises an exception" do
|
72
|
+
expect{ subject.create_or_update }.to raise_error(ActiveRecord::RecordNotUnique)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
describe RescueFromDuplicate::ActiveRecord do
|
79
|
+
if defined?(MysqlModel)
|
80
|
+
context 'mysql' do
|
81
|
+
let(:message) { "Duplicate entry '1-Rescuable-toto' for key 'index_rescuable_on_shop_id_and_type_and_name'" }
|
82
|
+
it_behaves_like 'database error rescuing'
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
if defined?(PostgresqlModel)
|
87
|
+
context 'pgsql' do
|
88
|
+
let(:message) { "PG::UniqueViolation: ERROR: duplicate key value violates unique constraint \"index_rescuable_on_shop_id_and_type_and_name\"\nDETAIL: Key (shop_id, type, name)=(1, Rescuable, toto) already exists.\n: INSERT INTO \"postgresql_models\" (\"shop_id\", \"type\", \"name\") VALUES ($1, $2, $3) RETURNING \"id\"" }
|
89
|
+
it_behaves_like 'database error rescuing'
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
context 'sqlite3' do
|
94
|
+
let(:message) { "SQLite3::ConstraintException: column shop_id, type, name is not unique: INSERT INTO \"sqlite3_models\" (\"shop_id\", \"type\", \"name\") VALUES (?, ?, ?)" }
|
95
|
+
it_behaves_like 'database error rescuing'
|
96
|
+
end
|
97
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'activerecord-rescue_from_duplicate'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'pry'
|
6
|
+
require 'pry-debugger'
|
7
|
+
rescue LoadError
|
8
|
+
end
|
9
|
+
|
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
|
+
|
22
|
+
|
23
|
+
module RescueFromDuplicate
|
24
|
+
class Base
|
25
|
+
cattr_accessor :exception
|
26
|
+
|
27
|
+
def create_or_update(*params)
|
28
|
+
raise self.class.exception if self.class.exception
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class Rescuable < Base
|
33
|
+
extend ActiveModel::Naming
|
34
|
+
extend ActiveModel::Translation
|
35
|
+
include ActiveModel::AttributeMethods
|
36
|
+
include RescueFromDuplicate::ActiveRecord::Extension
|
37
|
+
|
38
|
+
define_attribute_methods ['name']
|
39
|
+
attr_accessor :name
|
40
|
+
|
41
|
+
def self.table_name
|
42
|
+
"rescuable"
|
43
|
+
end
|
44
|
+
|
45
|
+
def read_attribute_for_validation(attribute)
|
46
|
+
send(attribute)
|
47
|
+
end
|
48
|
+
|
49
|
+
def _validators
|
50
|
+
self.class._validators
|
51
|
+
end
|
52
|
+
|
53
|
+
def errors
|
54
|
+
@errors ||= ActiveModel::Errors.new(self)
|
55
|
+
end
|
56
|
+
|
57
|
+
def self._validators
|
58
|
+
@validators ||= {
|
59
|
+
:name =>
|
60
|
+
[
|
61
|
+
uniqueness_validator,
|
62
|
+
presence_validator
|
63
|
+
]
|
64
|
+
}
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.uniqueness_validator
|
68
|
+
@uniqueness_validator ||= ::ActiveRecord::Validations::UniquenessValidator.new(
|
69
|
+
:attributes => [:name],
|
70
|
+
:case_sensitive => true, :scope => [:shop_id, :type],
|
71
|
+
:rescue_from_duplicate => true
|
72
|
+
).tap { |o| o.setup(self) if o.respond_to?(:setup) }
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.uniqueness_validator_without_rescue
|
76
|
+
@uniqueness_validator_without_rescue ||= ::ActiveRecord::Validations::UniquenessValidator.new(
|
77
|
+
:attributes => [:name],
|
78
|
+
:case_sensitive => true, :scope => [:shop_id, :type]
|
79
|
+
).tap { |o| o.setup(self) if o.respond_to?(:setup) }
|
80
|
+
end
|
81
|
+
|
82
|
+
def self.presence_validator
|
83
|
+
@presence_validator ||= ActiveModel::Validations::PresenceValidator.new(:attributes => [:name])
|
84
|
+
end
|
85
|
+
|
86
|
+
def self.index
|
87
|
+
@index ||= ::ActiveRecord::ConnectionAdapters::IndexDefinition.new(
|
88
|
+
"rescuable",
|
89
|
+
"index_rescuable_on_shop_id_and_type_and_name",
|
90
|
+
true,
|
91
|
+
["shop_id", "type", "name"],
|
92
|
+
[nil, nil, nil],
|
93
|
+
nil
|
94
|
+
)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
I18n.t(:prime)
|
100
|
+
I18n.backend.send(:translations)[:en][:errors][:messages][:taken] = "has already been taken"
|
101
|
+
Dir[File.expand_path(File.join(File.dirname(__FILE__), 'support', '**', '*.rb'))].each { |f| require f }
|
102
|
+
|
103
|
+
# This file was generated by the `rspec --init` command. Conventionally, all
|
104
|
+
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
105
|
+
# Require this file using `require "spec_helper"` to ensure that it is only
|
106
|
+
# loaded once.
|
107
|
+
#
|
108
|
+
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
109
|
+
RSpec.configure do |config|
|
110
|
+
config.treat_symbols_as_metadata_keys_with_true_values = true
|
111
|
+
config.run_all_when_everything_filtered = true
|
112
|
+
config.filter_run :focus
|
113
|
+
|
114
|
+
# Run specs in random order to surface order dependencies. If you find an
|
115
|
+
# order dependency and want to debug it, you can fix the order by providing
|
116
|
+
# the seed, which is printed after each run.
|
117
|
+
# --seed 1234
|
118
|
+
config.order = 'random'
|
119
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'json'
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
AR_VERSION = Gem::Version.new(ActiveRecord::VERSION::STRING)
|
6
|
+
AR_4_0 = Gem::Version.new('4.0')
|
7
|
+
AR_4_1 = Gem::Version.new('4.1.0.beta')
|
8
|
+
|
9
|
+
ActiveRecord::Base.configurations = {
|
10
|
+
'test_sqlite3' => {adapter: 'sqlite3', database: "/tmp/rescue_from_duplicate.db"},
|
11
|
+
'test_postgresql' => {adapter: 'postgresql', database: 'gmalette', username: 'gmalette', host: '127.0.0.1'},
|
12
|
+
'test_mysql' => {adapter: 'mysql2', database: 'rescue_from_duplicate', username: 'root', host: '127.0.0.1', port: 13306},
|
13
|
+
}
|
14
|
+
|
15
|
+
class CreateAllTables < ActiveRecord::Migration
|
16
|
+
def self.recreate_table(name, *args, &block)
|
17
|
+
execute "drop table if exists #{name}"
|
18
|
+
|
19
|
+
create_table(name, *args) do |t|
|
20
|
+
t.string :name
|
21
|
+
t.integer :size
|
22
|
+
end
|
23
|
+
|
24
|
+
add_index name, :name, :unique => true
|
25
|
+
add_index name, :size, :unique => true
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.up
|
29
|
+
if ENV['MYSQL']
|
30
|
+
ActiveRecord::Base.establish_connection('test_mysql')
|
31
|
+
recreate_table(:mysql_models)
|
32
|
+
end
|
33
|
+
|
34
|
+
if ENV['POSTGRES']
|
35
|
+
ActiveRecord::Base.establish_connection(ENV['POSTGRES_URL'] || 'test_postgresql')
|
36
|
+
recreate_table(:postgresql_models)
|
37
|
+
end
|
38
|
+
|
39
|
+
ActiveRecord::Base.establish_connection('test_sqlite3')
|
40
|
+
recreate_table(:sqlite3_models)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
ActiveRecord::Migration.verbose = false
|
45
|
+
CreateAllTables.up
|
46
|
+
|
47
|
+
|
48
|
+
module TestModel
|
49
|
+
extend ActiveSupport::Concern
|
50
|
+
|
51
|
+
included do
|
52
|
+
validates_uniqueness_of :name, :rescue_from_duplicate => true
|
53
|
+
validates_uniqueness_of :size
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
if ENV['MYSQL']
|
58
|
+
class MysqlModel < ActiveRecord::Base
|
59
|
+
include TestModel
|
60
|
+
|
61
|
+
establish_connection 'test_mysql'
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
if ENV['POSTGRES']
|
66
|
+
class PostgresqlModel < ActiveRecord::Base
|
67
|
+
include TestModel
|
68
|
+
|
69
|
+
establish_connection ENV['POSTGRES_URL'] || 'test_postgresql'
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
class Sqlite3Model < ActiveRecord::Base
|
74
|
+
include TestModel
|
75
|
+
|
76
|
+
establish_connection 'test_sqlite3'
|
77
|
+
end
|
78
|
+
|
79
|
+
Models = [
|
80
|
+
Sqlite3Model
|
81
|
+
]
|
82
|
+
Models << MysqlModel if defined?(MysqlModel)
|
83
|
+
Models << PostgresqlModel if defined?(PostgresqlModel)
|
84
|
+
|
85
|
+
|
86
|
+
RSpec.configure do |config|
|
87
|
+
config.before :each do
|
88
|
+
Models.each(&:delete_all)
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
shared_examples 'a model with rescued unique' do
|
4
|
+
describe 'create!' do
|
5
|
+
context 'when catching a race condition' do
|
6
|
+
|
7
|
+
before(:each) {
|
8
|
+
ActiveRecord::Validations::UniquenessValidator.any_instance.stub(:validate_each => nil)
|
9
|
+
described_class.create!(:name => 'toto', :size => 5)
|
10
|
+
}
|
11
|
+
|
12
|
+
it 'raises an ActiveRecord::RecordNotSaved error' do
|
13
|
+
expect{ described_class.create!(:name => 'toto') }.to raise_error(ActiveRecord::RecordNotSaved)
|
14
|
+
end
|
15
|
+
|
16
|
+
it "doesn't save the record" do
|
17
|
+
expect{
|
18
|
+
begin
|
19
|
+
described_class.create!(:name => 'toto')
|
20
|
+
rescue ActiveRecord::RecordNotSaved
|
21
|
+
# NOOP
|
22
|
+
end
|
23
|
+
}.not_to change(described_class, :count)
|
24
|
+
end
|
25
|
+
|
26
|
+
it "rollback the transaction" do
|
27
|
+
expect {
|
28
|
+
begin
|
29
|
+
described_class.transaction do
|
30
|
+
described_class.create(:name => 'not toto', :size => 55)
|
31
|
+
described_class.create!(:name => 'toto')
|
32
|
+
end
|
33
|
+
rescue ActiveRecord::RecordNotSaved
|
34
|
+
# NOOP
|
35
|
+
end
|
36
|
+
}.not_to change(described_class, :count)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
context "with no race condition" do
|
41
|
+
it 'saves the model' do
|
42
|
+
expect{ described_class.create!(:name => 'toto') }.to change(described_class, :count).by(1)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe Sqlite3Model do
|
49
|
+
it_behaves_like 'a model with rescued unique'
|
50
|
+
end
|
51
|
+
|
52
|
+
if defined?(MysqlModel)
|
53
|
+
describe MysqlModel do
|
54
|
+
it_behaves_like 'a model with rescued unique'
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
if defined?(PostgresqlModel)
|
59
|
+
describe PostgresqlModel do
|
60
|
+
it_behaves_like 'a model with rescued unique'
|
61
|
+
end
|
62
|
+
end
|
metadata
ADDED
@@ -0,0 +1,212 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: activerecord-rescue_from_duplicate
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Guillaume Malette
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-11-26 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activerecord
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ! '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '3.2'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ! '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '3.2'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ~>
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.3'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ~>
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.3'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ! '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: coveralls
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ! '>='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: sqlite3
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ! '>='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
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
|
+
- - ! '>='
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: mysql2
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ! '>='
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ! '>='
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: rspec
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ! '>='
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ! '>='
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: pry
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ! '>='
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ! '>='
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: pry-debugger
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ! '>='
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - ! '>='
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0'
|
153
|
+
description: Rescue from MySQL and Sqlite duplicate errors
|
154
|
+
email:
|
155
|
+
- guillaume@jadedpixel.com
|
156
|
+
executables: []
|
157
|
+
extensions: []
|
158
|
+
extra_rdoc_files: []
|
159
|
+
files:
|
160
|
+
- .gitignore
|
161
|
+
- .rspec
|
162
|
+
- .travis.yml
|
163
|
+
- Gemfile
|
164
|
+
- LICENSE.txt
|
165
|
+
- README.md
|
166
|
+
- Rakefile
|
167
|
+
- activerecord-rescue_from_duplicate.gemspec
|
168
|
+
- lib/activerecord-rescue_from_duplicate.rb
|
169
|
+
- lib/rescue_from_duplicate/active_record.rb
|
170
|
+
- lib/rescue_from_duplicate/active_record/extension.rb
|
171
|
+
- lib/rescue_from_duplicate/active_record/version.rb
|
172
|
+
- spec/gemfiles/Gemfile.ar-3.2
|
173
|
+
- spec/gemfiles/Gemfile.ar-4.0
|
174
|
+
- spec/gemfiles/Gemfile.ar-edge
|
175
|
+
- spec/rescue_from_duplicate_spec.rb
|
176
|
+
- spec/spec_helper.rb
|
177
|
+
- spec/support/model.rb
|
178
|
+
- spec/validator_spec.rb
|
179
|
+
homepage: https://github.com/Shopify/activerecord-rescue_from_duplicate
|
180
|
+
licenses:
|
181
|
+
- MIT
|
182
|
+
metadata: {}
|
183
|
+
post_install_message:
|
184
|
+
rdoc_options: []
|
185
|
+
require_paths:
|
186
|
+
- lib
|
187
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
188
|
+
requirements:
|
189
|
+
- - ! '>='
|
190
|
+
- !ruby/object:Gem::Version
|
191
|
+
version: '0'
|
192
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
193
|
+
requirements:
|
194
|
+
- - ! '>='
|
195
|
+
- !ruby/object:Gem::Version
|
196
|
+
version: '0'
|
197
|
+
requirements: []
|
198
|
+
rubyforge_project:
|
199
|
+
rubygems_version: 2.1.9
|
200
|
+
signing_key:
|
201
|
+
specification_version: 4
|
202
|
+
summary: Rescue from MySQL and Sqlite duplicate errors when trying to insert records
|
203
|
+
that fail uniqueness validation
|
204
|
+
test_files:
|
205
|
+
- spec/gemfiles/Gemfile.ar-3.2
|
206
|
+
- spec/gemfiles/Gemfile.ar-4.0
|
207
|
+
- spec/gemfiles/Gemfile.ar-edge
|
208
|
+
- spec/rescue_from_duplicate_spec.rb
|
209
|
+
- spec/spec_helper.rb
|
210
|
+
- spec/support/model.rb
|
211
|
+
- spec/validator_spec.rb
|
212
|
+
has_rdoc:
|