rescue_unique_constraint-p 2.0.0

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 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