activerecord-rescue_from_duplicate 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
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
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in activerecord-rescue_from_duplicate.gemspec
4
+ gemspec
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,5 @@
1
+ module Activerecord
2
+ module RescueFromDuplicate
3
+ VERSION = "0.0.1"
4
+ end
5
+ end
@@ -0,0 +1,10 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'activerecord', '~> 3.2.15'
4
+ gem 'bundler', '~> 1.3'
5
+ gem 'rake'
6
+ gem 'rspec'
7
+ gem 'sqlite3'
8
+ gem 'pg', '~> 0.11'
9
+ gem 'mysql2'
10
+ gem 'coveralls', require: false
@@ -0,0 +1,10 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'activerecord', '~> 4.0.0'
4
+ gem 'bundler', '~> 1.3'
5
+ gem 'rake'
6
+ gem 'rspec'
7
+ gem 'sqlite3'
8
+ gem 'pg', '~> 0.11'
9
+ gem 'mysql2'
10
+ gem 'coveralls', require: false
@@ -0,0 +1,12 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'rails', github: 'rails/rails'
4
+ gem 'arel', github: 'rails/arel'
5
+
6
+ gem 'bundler', '~> 1.3'
7
+ gem 'rake'
8
+ gem 'rspec'
9
+ gem 'sqlite3'
10
+ gem 'pg', '~> 0.11'
11
+ gem 'mysql2'
12
+ gem 'coveralls', require: false
@@ -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
@@ -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: