rescue_unique_constraint-p 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 80aa5a0fcd1ea228a538fcd38a5b60faa9a9eebf2c5b13e58eb4f8fe71164518
4
+ data.tar.gz: 56df147f3109eaa13ec085fdd1136a75b260864f04b8ad8283b6c241780d8d27
5
+ SHA512:
6
+ metadata.gz: d21e4cd8a1d82e34d8a216d9c91ca29bd0985129e7916ea6c74935764a46f06bb626bb93c4f9a8825983a9f7649cfd3c7d97595f878f8d1f038073553231ee37
7
+ data.tar.gz: d49d0306c7c9640962a38a6863e684ef78fccf3a2e6ca7380dd82e1e2037d7bf13302b4b26913475f32e25a18d7d35e4facfcf5ee9c5461626b30f3e6dd59b26
data/.gitignore ADDED
@@ -0,0 +1,23 @@
1
+ *.gem
2
+ *.rbc
3
+ .tags
4
+ .bundle
5
+ .config
6
+ .yardoc
7
+ Gemfile.lock
8
+ InstalledFiles
9
+ _yardoc
10
+ coverage
11
+ doc/
12
+ lib/bundler/man
13
+ pkg
14
+ rdoc
15
+ spec/reports
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
19
+ *.bundle
20
+ *.so
21
+ *.o
22
+ *.a
23
+ mkmf.log
data/.rubocop.yml ADDED
@@ -0,0 +1,2 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.1
data/.ruby-gemset ADDED
@@ -0,0 +1 @@
1
+ rescue-unique-constraint
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.1.2
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in rescue_unique_constraint.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,13 @@
1
+ Copyright 2015 Reverb.com, LLC
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
data/Makefile ADDED
@@ -0,0 +1,3 @@
1
+ .PHONY: build
2
+ build: ## build local gem
3
+ gem build rescue_unique_constraint.gemspec
data/README.md ADDED
@@ -0,0 +1,86 @@
1
+ # RescueUniqueConstraint
2
+
3
+ ActiveRecord doesn't do a great job of rescuing ActiveRecord::RecordNotUnique
4
+ violations resulting from a duplicate entry on a database level unique constraint.
5
+
6
+ This gem automatically rescues the error and instead adds a validation error
7
+ on the field in question, making it behave as if you had a normal uniqueness
8
+ validation.
9
+
10
+ Note that if you have only a unique constraint in the database and no uniqueness validation in ActiveRecord, it
11
+ is possible for your object to validate but then fail to save.
12
+
13
+ See Usage for more info.
14
+
15
+ ## Installation
16
+
17
+ Add this line to your application's Gemfile:
18
+
19
+ gem 'rescue_unique_constraint'
20
+
21
+ And then execute:
22
+
23
+ $ bundle
24
+
25
+ Or install it yourself as:
26
+
27
+ $ gem install rescue_unique_constraint
28
+
29
+ ## Usage
30
+
31
+ Assuming you've added unique index:
32
+
33
+ ```ruby
34
+ class AddIndexToThing < ActiveRecord::Migration
35
+ disable_ddl_transaction!
36
+
37
+ def change
38
+ add_index :things, :somefield, unique: true, algorithm: :concurrently, name: "my_unique_index"
39
+ end
40
+ end
41
+ ```
42
+
43
+ Before:
44
+
45
+ ```ruby
46
+ class Thing < ActiveRecord::Base
47
+ end
48
+
49
+ thing = Thing.create(somefield: "foo")
50
+ dupe = Thing.create(somefield: "foo")
51
+ # => raises ActiveRecord::RecordNotUnique
52
+ ```
53
+
54
+ Note that if you have `validates :uniqueness` in your model, it will prevent
55
+ the RecordNotUnique from being raised in _some_ cases, but not all, as race
56
+ conditions between multiple processes will still cause duplicate entries to
57
+ enter your database.
58
+
59
+ After:
60
+
61
+ ```ruby
62
+ class Thing < ActiveRecord::Base
63
+ include RescueUniqueConstraint
64
+ rescue_unique_constraint index: "my_unique_index", field: "somefield"
65
+ end
66
+
67
+ thing = Thing.create(somefield: "foo")
68
+ dupe = Thing.create(somefield: "foo")
69
+ # => false
70
+ thing.errors[:somefield] == "somefield has already been taken"
71
+ # => true
72
+ # => raises ActiveRecord::RecordNotUnique
73
+ ```
74
+
75
+ ## Testing
76
+
77
+ You'll need a database that supports unique constraints.
78
+ This gem has been tested with PostgreSQL, MySQL and SQLite.
79
+
80
+ ## Contributing
81
+
82
+ 1. [Fork it](https://github.com/reverbdotcom/rescue-unique-constraint/fork)
83
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
84
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
85
+ 4. Push to the branch (`git push origin my-new-feature`)
86
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ begin
4
+ require "rspec/core/rake_task"
5
+ RSpec::Core::RakeTask.new(:spec)
6
+
7
+ task default: :spec
8
+ rescue LoadError
9
+ end
@@ -0,0 +1,9 @@
1
+ module RescueUniqueConstraint
2
+ module Adapter
3
+ class MysqlAdapter
4
+ def index_error?(index, error_message)
5
+ error_message[/#{index.name}/]
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module RescueUniqueConstraint
2
+ module Adapter
3
+ class PostgresqlAdapter
4
+ def index_error?(index, error_message)
5
+ error_message[/#{index.name}/]
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,19 @@
1
+ module RescueUniqueConstraint
2
+ module Adapter
3
+ class SqliteAdapter
4
+ def initialize(table_name)
5
+ @table_name = table_name
6
+ end
7
+
8
+ # Sample error message returned by ActiveRecord for Sqlite Unique exception:
9
+ # 'SQLite3::ConstraintException: UNIQUE constraint failed: things.code, things.score: INSERT INTO "things" ("name", "test", "code", "score") VALUES (?, ?, ?, ?)'
10
+ #
11
+ # Step1: extract column names from above message on which unique constraint failed.
12
+ # Step2: Check if this index's field is among those columns.
13
+ def index_error?(index, error_message)
14
+ column_names = error_message.scan(%r{(?<=#{@table_name}\.)\w+})
15
+ column_names.include?(index.field)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,9 @@
1
+ module RescueUniqueConstraint
2
+ class Index
3
+ attr_reader :name, :field
4
+ def initialize(name, field)
5
+ @name = name
6
+ @field = field
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,44 @@
1
+ module RescueUniqueConstraint
2
+ # Handles storing and matching [index, field] pairs to exceptions
3
+ class RescueHandler
4
+ def initialize(model)
5
+ @model = model
6
+ @indexes_to_rescue_on = []
7
+ end
8
+
9
+ def add_index(index, field)
10
+ indexes_to_rescue_on << Index.new(index, field)
11
+ end
12
+
13
+ def matching_indexes(e)
14
+ indexes = indexes_to_rescue_on.select do |index|
15
+ database_adapter.index_error?(index, e.message)
16
+ end
17
+ raise e unless indexes.any?
18
+ indexes
19
+ end
20
+
21
+ private
22
+
23
+ attr_reader :indexes_to_rescue_on, :model
24
+
25
+ def database_adapter
26
+ @_database_adapter ||= (
27
+ case database_name
28
+ when :mysql2
29
+ Adapter::MysqlAdapter.new
30
+ when :postgresql
31
+ Adapter::PostgresqlAdapter.new
32
+ when :sqlite
33
+ Adapter::SqliteAdapter.new(@model.table_name)
34
+ else
35
+ raise "Database (#{database_name}) not supported"
36
+ end
37
+ )
38
+ end
39
+
40
+ def database_name
41
+ model.connection.adapter_name.downcase.to_sym
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RescueUniqueConstraint
4
+ VERSION = "2.0.0"
5
+ end
@@ -0,0 +1,43 @@
1
+ require 'rescue_unique_constraint-p/version'
2
+ require 'rescue_unique_constraint-p/index'
3
+ require 'rescue_unique_constraint-p/rescue_handler'
4
+ require 'rescue_unique_constraint-p/adapter/mysql_adapter'
5
+ require 'rescue_unique_constraint-p/adapter/postgresql_adapter'
6
+ require 'rescue_unique_constraint-p/adapter/sqlite_adapter'
7
+ require 'active_record'
8
+
9
+ # Module which will rescue ActiveRecord::RecordNotUnique exceptions
10
+ # and add errors for indexes that are registered with
11
+ # rescue_unique_constraint(index:, field:)
12
+ module RescueUniqueConstraint
13
+ def self.included(base)
14
+ base.extend(ClassMethods)
15
+ end
16
+
17
+ # methods mixed into ActiveRecord class
18
+ module ClassMethods
19
+ def index_rescue_handler
20
+ @_index_rescue_handler ||= RescueUniqueConstraint::RescueHandler.new(self)
21
+ end
22
+
23
+ def rescue_unique_constraint(index:, field:)
24
+ unless method_defined?(:create_or_update_with_rescue)
25
+ define_method(:create_or_update_with_rescue) do |*|
26
+ begin
27
+ create_or_update_without_rescue
28
+ rescue ActiveRecord::RecordNotUnique => e
29
+ self.class.index_rescue_handler.matching_indexes(e).each do |matching_index|
30
+ errors.add(matching_index.field, :taken)
31
+ end
32
+ return false
33
+ end
34
+ true
35
+ end
36
+
37
+ alias_method :create_or_update_without_rescue, :create_or_update
38
+ alias_method :create_or_update, :create_or_update_with_rescue
39
+ end
40
+ index_rescue_handler.add_index(index, field)
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'rescue_unique_constraint-p/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "rescue_unique_constraint-p"
8
+ spec.version = RescueUniqueConstraint::VERSION
9
+ spec.authors = ["Tam Dang", "Yan Pritzker"]
10
+ spec.email = ["tam.dang@reverb.com","yan@reverb.com"]
11
+ spec.summary = %q{Turns ActiveRecord::RecordNotUnique errors into ActiveRecord errors}
12
+ spec.description = %q{Rescues unique constraint violations and turns them into ActiveRecord errors}
13
+ spec.homepage = "https://github.com/reverbdotcom/rescue_unique_contraint"
14
+ spec.license = "Apache 2.0"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
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", "< 8"
22
+
23
+ spec.add_development_dependency "bundler", "~> 2.2"
24
+ spec.add_development_dependency "rake", "~> 10.5"
25
+ spec.add_development_dependency "rspec", "~> 3.0"
26
+ spec.add_development_dependency "sqlite3", "~> 1.3"
27
+ spec.add_development_dependency 'pry'
28
+ spec.add_development_dependency 'pry-byebug'
29
+ spec.add_development_dependency 'gem-release'
30
+ end
@@ -0,0 +1,58 @@
1
+ require 'active_record'
2
+ require 'rescue_unique_constraint-p'
3
+
4
+ describe RescueUniqueConstraint do
5
+ before :all do
6
+ ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:")
7
+ ActiveRecord::Schema.verbose = false
8
+ ActiveRecord::Schema.define(:version => 1) do
9
+ create_table :things do |t|
10
+ t.string :name
11
+ t.string :test
12
+ t.integer :code
13
+ t.integer :score
14
+ end
15
+
16
+ add_index :things, :name, unique: true, name: "idx_things_on_name_unique"
17
+ add_index :things, :test, unique: true, name: "idx_things_on_test_unique"
18
+ add_index :things, [:code, :score], unique: true, name: "idx_things_on_code_and_score_unique"
19
+ end
20
+ end
21
+
22
+ class Thing < ActiveRecord::Base
23
+ include RescueUniqueConstraint
24
+ rescue_unique_constraint index: "idx_things_on_name_unique", field: "name"
25
+ rescue_unique_constraint index: "idx_things_on_test_unique", field: "test"
26
+ rescue_unique_constraint index: "idx_things_on_code_and_score_unique", field: "score"
27
+ end
28
+
29
+ before :each do
30
+ Thing.destroy_all
31
+ end
32
+
33
+ it "rescues unique constraint violations as activerecord errors" do
34
+ thing = Thing.create(name: "foo", test: 'bar', code: 123, score: 1000)
35
+ dupe = Thing.new(name: "foo", test: 'baz', code: 456, score: 2000)
36
+ expect(dupe.save).to eql false
37
+ expect(dupe.errors.messages.keys).to contain_exactly(:name)
38
+ expect(dupe.errors[:name].first).to match /has already been taken/
39
+ end
40
+
41
+ it "adds error message to atrribute which caused unique-voilation" do
42
+ thing = Thing.create(name: "foo", test: 'bar', code: 123, score: 1000)
43
+ dupe = Thing.new(name: "lorem", test: 'bar', code: 456, score: 2000)
44
+ expect(dupe.save).to eql false
45
+ expect(dupe.errors.messages.keys).to contain_exactly(:test)
46
+ expect(dupe.errors[:test].first).to match /has already been taken/
47
+ end
48
+
49
+ context "When unique contraint is voilated by a composite index" do
50
+ it "adds error message to user defined atrribute" do
51
+ thing = Thing.create(name: "foo", test: 'bar', code: 123, score: 1000)
52
+ dupe = Thing.new(name: "lorem", test: 'ipsum', code: 123, score: 1000)
53
+ expect(dupe.save).to eql false
54
+ expect(dupe.errors.messages.keys).to contain_exactly(:score)
55
+ expect(dupe.errors[:score].first).to match /has already been taken/
56
+ end
57
+ end
58
+ end
metadata ADDED
@@ -0,0 +1,183 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rescue_unique_constraint-p
3
+ version: !ruby/object:Gem::Version
4
+ version: 2.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Tam Dang
8
+ - Yan Pritzker
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2022-06-07 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activerecord
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ">="
19
+ - !ruby/object:Gem::Version
20
+ version: '3.2'
21
+ - - "<"
22
+ - !ruby/object:Gem::Version
23
+ version: '8'
24
+ type: :runtime
25
+ prerelease: false
26
+ version_requirements: !ruby/object:Gem::Requirement
27
+ requirements:
28
+ - - ">="
29
+ - !ruby/object:Gem::Version
30
+ version: '3.2'
31
+ - - "<"
32
+ - !ruby/object:Gem::Version
33
+ version: '8'
34
+ - !ruby/object:Gem::Dependency
35
+ name: bundler
36
+ requirement: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.2'
41
+ type: :development
42
+ prerelease: false
43
+ version_requirements: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '2.2'
48
+ - !ruby/object:Gem::Dependency
49
+ name: rake
50
+ requirement: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '10.5'
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '10.5'
62
+ - !ruby/object:Gem::Dependency
63
+ name: rspec
64
+ requirement: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.0'
69
+ type: :development
70
+ prerelease: false
71
+ version_requirements: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.0'
76
+ - !ruby/object:Gem::Dependency
77
+ name: sqlite3
78
+ requirement: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.3'
83
+ type: :development
84
+ prerelease: false
85
+ version_requirements: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.3'
90
+ - !ruby/object:Gem::Dependency
91
+ name: pry
92
+ requirement: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ type: :development
98
+ prerelease: false
99
+ version_requirements: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ - !ruby/object:Gem::Dependency
105
+ name: pry-byebug
106
+ requirement: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ type: :development
112
+ prerelease: false
113
+ version_requirements: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ - !ruby/object:Gem::Dependency
119
+ name: gem-release
120
+ requirement: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ type: :development
126
+ prerelease: false
127
+ version_requirements: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ description: Rescues unique constraint violations and turns them into ActiveRecord
133
+ errors
134
+ email:
135
+ - tam.dang@reverb.com
136
+ - yan@reverb.com
137
+ executables: []
138
+ extensions: []
139
+ extra_rdoc_files: []
140
+ files:
141
+ - ".gitignore"
142
+ - ".rubocop.yml"
143
+ - ".ruby-gemset"
144
+ - ".ruby-version"
145
+ - Gemfile
146
+ - LICENSE.txt
147
+ - Makefile
148
+ - README.md
149
+ - Rakefile
150
+ - lib/rescue_unique_constraint-p.rb
151
+ - lib/rescue_unique_constraint-p/adapter/mysql_adapter.rb
152
+ - lib/rescue_unique_constraint-p/adapter/postgresql_adapter.rb
153
+ - lib/rescue_unique_constraint-p/adapter/sqlite_adapter.rb
154
+ - lib/rescue_unique_constraint-p/index.rb
155
+ - lib/rescue_unique_constraint-p/rescue_handler.rb
156
+ - lib/rescue_unique_constraint-p/version.rb
157
+ - rescue_unique_constraint-p.gemspec
158
+ - spec/rescue_unique_constraint_spec-p.rb
159
+ homepage: https://github.com/reverbdotcom/rescue_unique_contraint
160
+ licenses:
161
+ - Apache 2.0
162
+ metadata: {}
163
+ post_install_message:
164
+ rdoc_options: []
165
+ require_paths:
166
+ - lib
167
+ required_ruby_version: !ruby/object:Gem::Requirement
168
+ requirements:
169
+ - - ">="
170
+ - !ruby/object:Gem::Version
171
+ version: '0'
172
+ required_rubygems_version: !ruby/object:Gem::Requirement
173
+ requirements:
174
+ - - ">="
175
+ - !ruby/object:Gem::Version
176
+ version: '0'
177
+ requirements: []
178
+ rubygems_version: 3.3.7
179
+ signing_key:
180
+ specification_version: 4
181
+ summary: Turns ActiveRecord::RecordNotUnique errors into ActiveRecord errors
182
+ test_files:
183
+ - spec/rescue_unique_constraint_spec-p.rb