database_validations 0.7.3 → 0.8.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2236241fbd9c25b9cbe9ffa63f5634659c6ad0ded206324cc7021765049ae116
4
- data.tar.gz: ec3e6c0ec0ea21a00455a2035452030977994e99bdb4b4ff45b8405d57e5f0d4
3
+ metadata.gz: 047eabab521a0b04f0385213e7b20a34cb192576935d3e9c2f70aeab28df6ca0
4
+ data.tar.gz: 61fe8cac30624557d22ff059b2738ccdddbf0bbaa99eb9ac67a74f943fe39abf
5
5
  SHA512:
6
- metadata.gz: f5691f17f29a775e910150be7415c77a02eb0436e428bf14efa60365f0840574066307b02d0b71de00431dff71f24215e366a68d44f1cf34528efb3570a584ec
7
- data.tar.gz: 613e741dc4f48e245f3d6abc9af136fad6ae28d974583e2713ce9af4955d68c97d73fb371444e8fc519bfa428758221680dffc77b48e91a1d9b62103a2291d5a
6
+ metadata.gz: c99d04c4d975a9f50781422c409fa2e08fe3cc0fbc2ec8bdc015ab766b8c7e45ed646de43cf30c3c23512cc01877462cd16896034b03e390c3fb7309242c0fc0
7
+ data.tar.gz: '098fa9d49805b3379fa0fa5b95d3c93771be64b709c7cda5c42aecdf2fa33945bf59d3076a7035f548d1dd0de362d9684053330db5118635a1fef60bc1124f57'
@@ -46,7 +46,7 @@ RSpec::Matchers.define :validate_db_uniqueness_of do |field|
46
46
 
47
47
  model = object.is_a?(Class) ? object : object.class
48
48
 
49
- DatabaseValidations::Helpers.each_validator(model) do |validator|
49
+ DatabaseValidations::Helpers.each_uniqueness_validator(model) do |validator|
50
50
  @validators << {
51
51
  field: validator.field,
52
52
  scope: validator.scope,
@@ -23,6 +23,14 @@ module DatabaseValidations
23
23
  model.connection.indexes(model.table_name).select(&:unique)
24
24
  end
25
25
 
26
+ def foreign_keys
27
+ model.connection.foreign_keys(model.table_name)
28
+ end
29
+
30
+ def find_foreign_key_by_column(column)
31
+ model.connection.foreign_key_exists?(model.table_name, column: column)
32
+ end
33
+
26
34
  # @return [Symbol]
27
35
  def adapter_name
28
36
  self.class::ADAPTER
@@ -8,9 +8,13 @@ module DatabaseValidations
8
8
  error_message[/key '([^']+)'/, 1]
9
9
  end
10
10
 
11
- def error_columns(error_message)
11
+ def unique_error_columns(error_message)
12
12
  find_index_by_name(index_name(error_message)).columns
13
13
  end
14
+
15
+ def foreign_key_error_column(error_message)
16
+ error_message[/FOREIGN KEY \(`([^`]+)`\)/, 1]
17
+ end
14
18
  end
15
19
  end
16
20
  end
@@ -8,9 +8,13 @@ module DatabaseValidations
8
8
  error_message[/unique constraint "([^"]+)"/, 1]
9
9
  end
10
10
 
11
- def error_columns(error_message)
11
+ def unique_error_columns(error_message)
12
12
  find_index_by_name(index_name(error_message)).columns
13
13
  end
14
+
15
+ def foreign_key_error_column(error_message)
16
+ error_message[/Key \(([^)]+)\)/, 1]
17
+ end
14
18
  end
15
19
  end
16
20
  end
@@ -7,9 +7,17 @@ module DatabaseValidations
7
7
  def index_name(_error_message)
8
8
  end
9
9
 
10
- def error_columns(error_message)
10
+ def find_foreign_key_by_column(column)
11
+ foreign_keys.find { |foreign_key| foreign_key.column.to_s == column.to_s }
12
+ end
13
+
14
+ def unique_error_columns(error_message)
11
15
  error_message.scan(/#{model.table_name}\.([^,:]+)/).flatten
12
16
  end
17
+
18
+ def foreign_key_error_column(error_message)
19
+ error_message[/\("([^"]+)"\) VALUES/, 1]
20
+ end
13
21
  end
14
22
  end
15
23
  end
@@ -0,0 +1,58 @@
1
+ module DatabaseValidations
2
+ module BelongsToHandlers
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ alias_method :validate, :valid?
7
+ end
8
+
9
+ def valid?(context = nil)
10
+ output = super(context)
11
+
12
+ Helpers.each_belongs_to_presence_validator(self.class) do |validator|
13
+ validates_with(ActiveRecord::Validations::PresenceValidator, validator.validates_presence_options)
14
+ end
15
+
16
+ errors.empty? && output
17
+ end
18
+
19
+ def save(opts = {})
20
+ return false unless Helpers.check_foreign_key_missing(self)
21
+
22
+ ActiveRecord::Base.connection.transaction(requires_new: true) { super }
23
+ rescue ActiveRecord::InvalidForeignKey => e
24
+ Helpers.handle_foreign_key_error!(self, e)
25
+ false
26
+ end
27
+
28
+ def save!(opts = {})
29
+ raise ActiveRecord::RecordInvalid, self unless Helpers.check_foreign_key_missing(self)
30
+
31
+ ActiveRecord::Base.connection.transaction(requires_new: true) { super }
32
+ rescue ActiveRecord::InvalidForeignKey => e
33
+ Helpers.handle_foreign_key_error!(self, e)
34
+ raise ActiveRecord::RecordInvalid, self
35
+ end
36
+
37
+ private
38
+
39
+ def perform_validations(options = {})
40
+ options[:validate] == false || valid_without_database_validations(options[:context])
41
+ end
42
+ end
43
+
44
+ module ClassMethods
45
+ def db_belongs_to(name, scope = nil, **options)
46
+ include(DatabaseValidations::ValidWithoutDatabaseValidations)
47
+ @database_validations_opts ||= DatabaseValidations::OptionsStorage.new(self)
48
+
49
+ belongs_to(name, scope, options.merge(optional: true))
50
+
51
+ foreign_key = reflections[name.to_s].foreign_key
52
+
53
+ @database_validations_opts.push_belongs_to(foreign_key, name)
54
+
55
+ include(DatabaseValidations::BelongsToHandlers)
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,43 @@
1
+ module DatabaseValidations
2
+ class BelongsToOptions
3
+ attr_reader :column, :adapter, :relation
4
+
5
+ def initialize(column, relation, adapter)
6
+ @column = column
7
+ @relation = relation
8
+ @adapter = adapter
9
+
10
+ raise_if_unsupported_database!
11
+ raise_if_foreign_key_missed! unless ENV['SKIP_DB_UNIQUENESS_VALIDATOR_INDEX_CHECK']
12
+ end
13
+
14
+ # @return [String]
15
+ def key
16
+ Helpers.generate_key_for_belongs_to(column)
17
+ end
18
+
19
+ def handle_foreign_key_error(instance)
20
+ # Hack to not query the database because we know the result already
21
+ instance.send("#{relation}=", nil)
22
+ instance.errors.add(relation, :blank, message: :required)
23
+ end
24
+
25
+ def validates_presence_options
26
+ {attributes: relation, message: :required}
27
+ end
28
+
29
+ private
30
+
31
+ def raise_if_foreign_key_missed!
32
+ unless adapter.find_foreign_key_by_column(column)
33
+ raise Errors::ForeignKeyNotFound.new(column, adapter.foreign_keys)
34
+ end
35
+ end
36
+
37
+ def raise_if_unsupported_database!
38
+ if adapter.adapter_name == :sqlite3
39
+ raise Errors::UnsupportedDatabase.new(:db_belongs_to, adapter.adapter_name)
40
+ end
41
+ end
42
+ end
43
+ end
@@ -1,6 +1,10 @@
1
1
  module DatabaseValidations
2
2
  module Errors
3
- class Base < StandardError; end
3
+ class Base < StandardError
4
+ def env_message
5
+ "Use ENV['SKIP_DB_UNIQUENESS_VALIDATOR_INDEX_CHECK']=true in case you want to skip the check. For example, when you run migrations."
6
+ end
7
+ end
4
8
 
5
9
  class IndexNotFound < Base
6
10
  attr_reader :columns, :where_clause, :index_name, :available_indexes
@@ -19,7 +23,7 @@ module DatabaseValidations
19
23
  "Available indexes are: [#{self.available_indexes.map { |ind| columns_and_where_text(ind.columns, ind.where) }.join(', ')}]. "
20
24
  end
21
25
 
22
- super text + "Use ENV['SKIP_DB_UNIQUENESS_VALIDATOR_INDEX_CHECK']=true in case you want to skip the check. For example, when you run migrations."
26
+ super text + env_message
23
27
  end
24
28
 
25
29
  def columns_and_where_text(columns, where)
@@ -46,5 +50,26 @@ module DatabaseValidations
46
50
  super "Option #{self.option} is not supported for #{self.database}. Supported options are: #{self.supported_options}"
47
51
  end
48
52
  end
53
+
54
+ class ForeignKeyNotFound < Base
55
+ attr_reader :column, :foreign_keys
56
+
57
+ def initialize(column, foreign_keys)
58
+ @column = column
59
+ @foreign_keys = foreign_keys
60
+
61
+ super "No foreign key found with column: \"#{column}\". Founded foreign keys are: #{foreign_keys}. " + env_message
62
+ end
63
+ end
64
+
65
+ class UnsupportedDatabase < Base
66
+ attr_reader :database, :method
67
+
68
+ def initialize(method, database)
69
+ @database = database
70
+ @method = method
71
+ super "Database #{database} doesn't support #{method}"
72
+ end
73
+ end
49
74
  end
50
75
  end
@@ -4,8 +4,8 @@ module DatabaseValidations
4
4
 
5
5
  def handle_unique_error!(instance, error)
6
6
  adapter = Adapters.factory(instance.class)
7
- index_key = adapter.index_name(error.message)
8
- column_key = generate_key(adapter.error_columns(error.message))
7
+ index_key = generate_key_for_uniqueness_index(adapter.index_name(error.message))
8
+ column_key = generate_key_for_uniqueness(adapter.unique_error_columns(error.message))
9
9
 
10
10
  each_options_storage(instance.class) do |storage|
11
11
  return storage[index_key].handle_unique_error(instance) if storage[index_key]
@@ -15,17 +15,43 @@ module DatabaseValidations
15
15
  raise error
16
16
  end
17
17
 
18
+ def handle_foreign_key_error!(instance, error)
19
+ adapter = Adapters.factory(instance.class)
20
+ column_key = generate_key_for_belongs_to(adapter.foreign_key_error_column(error.message))
21
+
22
+ each_options_storage(instance.class) do |storage|
23
+ return storage[column_key].handle_foreign_key_error(instance) if storage[column_key]
24
+ end
25
+
26
+ raise error
27
+ end
28
+
29
+ def check_foreign_key_missing(instance)
30
+ Helpers.each_belongs_to_presence_validator(instance.class) do |validator|
31
+ if instance.public_send(validator.column).nil? && instance.public_send(validator.relation).nil?
32
+ instance.errors.add(validator.relation, :blank, message: :required)
33
+ end
34
+ end
35
+ instance.errors.empty?
36
+ end
37
+
18
38
  def each_options_storage(klass)
19
39
  while klass.respond_to?(:validates_db_uniqueness_of)
20
- storage = klass.instance_variable_get(:'@validates_db_uniqueness_opts')
40
+ storage = klass.instance_variable_get(:'@database_validations_opts')
21
41
  yield(storage) if storage
22
42
  klass = klass.superclass
23
43
  end
24
44
  end
25
45
 
26
- def each_validator(klass)
46
+ def each_uniqueness_validator(klass)
27
47
  each_options_storage(klass) do |storage|
28
- storage.each_validator { |validator| yield(validator) }
48
+ storage.each_uniqueness_validator { |validator| yield(validator) }
49
+ end
50
+ end
51
+
52
+ def each_belongs_to_presence_validator(klass)
53
+ each_options_storage(klass) do |storage|
54
+ storage.each_belongs_to_presence_validator { |validator| yield(validator) }
29
55
  end
30
56
  end
31
57
 
@@ -33,8 +59,20 @@ module DatabaseValidations
33
59
  columns.flatten.compact.map(&:to_s).sort
34
60
  end
35
61
 
36
- def generate_key(*columns)
37
- unify_columns(columns).join('__')
62
+ def generate_key_for_uniqueness_index(index_name)
63
+ generate_key(:uniqueness_index, index_name)
64
+ end
65
+
66
+ def generate_key_for_uniqueness(*columns)
67
+ generate_key(:uniqueness, columns)
68
+ end
69
+
70
+ def generate_key_for_belongs_to(column)
71
+ generate_key(:belongs_to, column)
72
+ end
73
+
74
+ def generate_key(type, *columns)
75
+ [type, *unify_columns(columns)].join('__')
38
76
  end
39
77
  end
40
78
  end
@@ -0,0 +1,35 @@
1
+ module DatabaseValidations
2
+ class OptionsStorage
3
+
4
+ def initialize(klass)
5
+ @adapter = Adapters.factory(klass)
6
+ @storage = {}
7
+ end
8
+
9
+ def push_uniqueness(field, options)
10
+ uniqueness_options = UniquenessOptions.new(field, options, adapter)
11
+ storage[uniqueness_options.key] = uniqueness_options
12
+ end
13
+
14
+ def push_belongs_to(field, relation)
15
+ belongs_to_options = BelongsToOptions.new(field, relation, adapter)
16
+ storage[belongs_to_options.key] = belongs_to_options
17
+ end
18
+
19
+ def [](key)
20
+ storage[key]
21
+ end
22
+
23
+ def each_uniqueness_validator
24
+ storage.values.grep(UniquenessOptions).each { |validator| yield(validator) }
25
+ end
26
+
27
+ def each_belongs_to_presence_validator
28
+ storage.values.grep(BelongsToOptions).each { |validator| yield(validator) }
29
+ end
30
+
31
+ private
32
+
33
+ attr_reader :storage, :adapter
34
+ end
35
+ end
@@ -3,21 +3,19 @@ module DatabaseValidations
3
3
  extend ActiveSupport::Concern
4
4
 
5
5
  included do
6
- alias_method :valid_without_uniqueness?, :valid?
6
+ alias_method :validate, :valid?
7
+ end
7
8
 
8
- def valid?(context = nil)
9
- output = super(context)
9
+ def valid?(context = nil)
10
+ output = super(context)
10
11
 
11
- Helpers.each_validator(self.class) do |validator|
12
- if validator.if_and_unless_pass?(self)
13
- validates_with(ActiveRecord::Validations::UniquenessValidator, validator.validates_uniqueness_options)
14
- end
12
+ Helpers.each_uniqueness_validator(self.class) do |validator|
13
+ if validator.if_and_unless_pass?(self)
14
+ validates_with(ActiveRecord::Validations::UniquenessValidator, validator.validates_uniqueness_options)
15
15
  end
16
-
17
- errors.empty? && output
18
16
  end
19
17
 
20
- alias_method :validate, :valid?
18
+ errors.empty? && output
21
19
  end
22
20
 
23
21
  def save(options = {})
@@ -37,18 +35,19 @@ module DatabaseValidations
37
35
  private
38
36
 
39
37
  def perform_validations(options = {})
40
- options[:validate] == false || valid_without_uniqueness?(options[:context])
38
+ options[:validate] == false || valid_without_database_validations(options[:context])
41
39
  end
42
40
  end
43
41
 
44
42
  module ClassMethods
45
43
  def validates_db_uniqueness_of(*attributes)
46
- @validates_db_uniqueness_opts ||= DatabaseValidations::UniquenessOptionsStorage.new(self)
44
+ include(DatabaseValidations::ValidWithoutDatabaseValidations)
45
+ @database_validations_opts ||= DatabaseValidations::OptionsStorage.new(self)
47
46
 
48
47
  options = attributes.extract_options!
49
48
 
50
49
  attributes.each do |attribute|
51
- @validates_db_uniqueness_opts.push(attribute, options.merge(attributes: attribute))
50
+ @database_validations_opts.push_uniqueness(attribute, options.merge(attributes: attribute))
52
51
  end
53
52
 
54
53
  include(DatabaseValidations::UniquenessHandlers)
@@ -39,7 +39,7 @@ module DatabaseValidations
39
39
 
40
40
  # @return [String]
41
41
  def key
42
- @key ||= index_name ? index_name.to_s : Helpers.generate_key(columns)
42
+ @key ||= index_name ? Helpers.generate_key_for_uniqueness_index(index_name) : Helpers.generate_key_for_uniqueness(columns)
43
43
  end
44
44
 
45
45
  # @return [Array<String>]
@@ -0,0 +1,9 @@
1
+ module DatabaseValidations
2
+ module ValidWithoutDatabaseValidations
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ alias_method :valid_without_database_validations, :valid?
7
+ end
8
+ end
9
+ end
@@ -1,3 +1,3 @@
1
1
  module DatabaseValidations
2
- VERSION = '0.7.3'
2
+ VERSION = '0.8.0'
3
3
  end
@@ -4,9 +4,14 @@ require 'database_validations/version'
4
4
 
5
5
  require 'database_validations/rails/railtie' if defined?(Rails)
6
6
 
7
- require 'database_validations/validations/uniqueness_validator'
7
+ require 'database_validations/validations/uniqueness_handlers'
8
8
  require 'database_validations/validations/uniqueness_options'
9
- require 'database_validations/validations/uniqueness_options_storage'
9
+
10
+ require 'database_validations/validations/belongs_to_handlers'
11
+ require 'database_validations/validations/belongs_to_options'
12
+
13
+ require 'database_validations/validations/valid_without_database_validations'
14
+ require 'database_validations/validations/options_storage'
10
15
  require 'database_validations/validations/errors'
11
16
  require 'database_validations/validations/helpers'
12
17
  require 'database_validations/validations/adapters'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: database_validations
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.3
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Evgeniy Demin
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-10-18 00:00:00.000000000 Z
11
+ date: 2018-11-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -155,8 +155,6 @@ executables: []
155
155
  extensions: []
156
156
  extra_rdoc_files: []
157
157
  files:
158
- - LICENSE.txt
159
- - README.md
160
158
  - lib/database_validations.rb
161
159
  - lib/database_validations/rails/railtie.rb
162
160
  - lib/database_validations/rspec/matchers.rb
@@ -167,11 +165,14 @@ files:
167
165
  - lib/database_validations/validations/adapters/mysql_adapter.rb
168
166
  - lib/database_validations/validations/adapters/postgresql_adapter.rb
169
167
  - lib/database_validations/validations/adapters/sqlite_adapter.rb
168
+ - lib/database_validations/validations/belongs_to_handlers.rb
169
+ - lib/database_validations/validations/belongs_to_options.rb
170
170
  - lib/database_validations/validations/errors.rb
171
171
  - lib/database_validations/validations/helpers.rb
172
+ - lib/database_validations/validations/options_storage.rb
173
+ - lib/database_validations/validations/uniqueness_handlers.rb
172
174
  - lib/database_validations/validations/uniqueness_options.rb
173
- - lib/database_validations/validations/uniqueness_options_storage.rb
174
- - lib/database_validations/validations/uniqueness_validator.rb
175
+ - lib/database_validations/validations/valid_without_database_validations.rb
175
176
  - lib/database_validations/version.rb
176
177
  homepage: https://github.com/toptal/database_validations
177
178
  licenses:
@@ -193,7 +194,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
193
194
  version: '0'
194
195
  requirements: []
195
196
  rubyforge_project:
196
- rubygems_version: 2.7.7
197
+ rubygems_version: 2.7.8
197
198
  signing_key:
198
199
  specification_version: 4
199
200
  summary: Provide compatibility between database constraints and ActiveRecord validations
data/LICENSE.txt DELETED
@@ -1,21 +0,0 @@
1
- The MIT License (MIT)
2
-
3
- Copyright (c) 2018 Toptal
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in
13
- all copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
- THE SOFTWARE.
data/README.md DELETED
@@ -1,212 +0,0 @@
1
- # DatabaseValidations
2
-
3
- [![Build Status](https://travis-ci.org/toptal/database_validations.svg?branch=master)](https://travis-ci.org/toptal/database_validations)
4
- [![Gem Version](https://badge.fury.io/rb/database_validations.svg)](https://badge.fury.io/rb/database_validations)
5
-
6
- ActiveRecord provides validations on app level but it won't guarantee the
7
- consistent. In some cases, like `validates_uniqueness_of` it executes
8
- additional SQL query to the database and that is not very efficient.
9
-
10
- The main goal of the gem is to provide compatibility between database constraints
11
- and ActiveRecord validations with better performance and consistency.
12
-
13
- ## Installation
14
-
15
- Add this line to your application's Gemfile:
16
-
17
- ```ruby
18
- gem 'database_validations'
19
- ```
20
-
21
- And then execute:
22
-
23
- $ bundle
24
-
25
- Or install it yourself as:
26
-
27
- $ gem install database_validations
28
-
29
- ## validates_db_uniqueness_of
30
-
31
- Supported databases: `postgresql`, `mysql` and `sqlite`.
32
-
33
- ### Pros and Cons
34
-
35
- Advantages:
36
- - Provides true uniqueness on the database level because it handles race conditions cases properly.
37
- - Check the existence of correct unique index at the boot time. Use `ENV['SKIP_DB_UNIQUENESS_VALIDATOR_INDEX_CHECK'] = 'true'`
38
- if you want to skip it in some cases.
39
- - It's faster. See [Benchmark](https://github.com/toptal/database_validations#benchmark-code) section for details.
40
-
41
- Disadvantages:
42
- - Cannot handle multiple validations at once because database raises only one error for all indexes per query.
43
- ```ruby
44
- class User < ActiveRecord::Base
45
- validates_db_uniqueness_of :email, :name
46
- end
47
-
48
- original = User.create(name: 'name', email: 'email@mail.com')
49
- dupe = User.create(name: 'name', email: 'email@mail.com')
50
- # => false
51
- dupe.errors.messages
52
- # => {:name=>["has already been taken"]}
53
- ```
54
-
55
- ### How it works?
56
-
57
- We override `save` and `save!` methods where we rescue `ActiveRecord::RecordNotUnique` and add proper errors
58
- for compatibility.
59
-
60
- For `valid?` we use implementation from `validates_uniqueness_of` where we query the database.
61
-
62
- ### Usage
63
-
64
- ```ruby
65
- class User < ActiveRecord::Base
66
- validates_db_uniqueness_of :email
67
- end
68
-
69
- original = User.create(email: 'email@mail.com')
70
- dupe = User.create(email: 'email@mail.com')
71
- # => false
72
- dupe.errors.messages
73
- # => {:email=>["has already been taken"]}
74
- User.create!(email: 'email@mail.com')
75
- # => ActiveRecord::RecordInvalid Validation failed: email has already been taken
76
- ```
77
-
78
- ### Configuration options
79
-
80
- We want to provide full compatibility with existing `validates_uniqueness_of` validator.
81
-
82
- | Option name | PostgreSQL | MySQL | SQLite |
83
- | -------------- | :--------: | :---: | :----: |
84
- | scope | + | + | + |
85
- | message | + | + | + |
86
- | if | + | + | + |
87
- | unless | + | + | + |
88
- | index_name | + | + | - |
89
- | where | + | - | - |
90
- | case_sensitive | + | - | - |
91
- | allow_nil | - | - | - |
92
- | allow_blank | - | - | - |
93
-
94
- **Keep in mind**: `if`, `unless` and `case_sensitive` options are used only for `valid?` method.
95
-
96
- ```ruby
97
- class User < ActiveRecord::Base
98
- validates_db_uniqueness_of :email, if: -> { email && email_changed? }
99
- end
100
-
101
- user = User.create(email: 'email@mail.com', field: 'field')
102
- user.field = 'another'
103
-
104
- user.valid? # Will not query the database
105
- ```
106
-
107
- **Backward compatibility**: Even when we don't natively support `case_sensitive`, `allow_nil` and `allow_blank` options now, the following:
108
-
109
- ```ruby
110
- validates_db_uniqueness_of :email
111
- ```
112
-
113
- Is the same by default as the following
114
-
115
- ```ruby
116
- validates_uniqueness_of :email, allow_nil: true, allow_blank: false, case_sensitive: true
117
- ```
118
-
119
- Complete `case_sensitive` replacement example (for `PostgreSQL` only):
120
-
121
- ```ruby
122
- validates :slug, uniqueness: { case_sensitive: false, scope: :field }
123
- ```
124
-
125
- Should be replaced by:
126
-
127
- ```ruby
128
- validates_db_uniqueness_of :slug, index_name: :unique_index_with_field_lower_on_slug, case_sensitive: false
129
- ```
130
-
131
- Options descriptions:
132
- - `scope`: One or more columns by which to limit the scope of the uniqueness constraint.
133
- - `message`: Specifies a custom error message (default is: "has already been taken").
134
- - `if`: Specifies a method or proc to call to determine if the validation should occur
135
- (e.g. `if: :allow_validation`, or `if: Proc.new { |user| user.signup_step > 2 }`). The method or
136
- proc should return or evaluate to a `true` or `false` value.
137
- - `unless`: Specifies a method or proc to call to determine if the validation should not
138
- occur (e.g. `unless: :skip_validation`, or `unless: Proc.new { |user| user.signup_step <= 2 }`).
139
- The method or proc should return or evaluate to a `true` or `false` value.
140
- - `where`: Specify the conditions to be included as a `WHERE` SQL fragment to
141
- limit the uniqueness constraint lookup (e.g. `where: "(status = 'active')"`).
142
- For backward compatibility, this will be converted automatically
143
- to `conditions: -> { where("(status = 'active')") }` for `valid?` method.
144
- - `case_sensitive`: Looks for an exact match. Ignored by non-text columns (`true` by default).
145
- - `allow_nil`: If set to `true`, skips this validation if the attribute is `nil` (default is `false`).
146
- - `allow_blank`: If set to `true`, skips this validation if the attribute is blank (default is `false`).
147
- - `index_name`: Allows to make explicit connection between validator and index. Used when gem can't automatically find index.
148
-
149
- ### Benchmark ([code](https://github.com/toptal/database_validations/blob/master/benchmarks/uniqueness_validator_benchmark.rb))
150
-
151
- | Case | Validator | SQLite | PostgreSQL | MySQL |
152
- | -------------------------------- | -------------------------- | ------------------------------------------ | ------------------------------------------ | ------------------------------------------ |
153
- | Save duplicate item only | validates_db_uniqueness_of | 1.404k (±14.7%) i/s - 6.912k in 5.043409s | 508.889 (± 2.8%) i/s - 2.550k in 5.015044s | 649.356 (±11.5%) i/s - 3.283k in 5.153444s |
154
- | | validates_uniqueness_of | 1.505k (±14.6%) i/s - 7.448k in 5.075696s | 637.017 (±14.1%) i/s - 3.128k in 5.043434s | 473.561 (± 9.7%) i/s - 2.352k in 5.021151s |
155
- | Save unique item only | validates_db_uniqueness_of | 3.241k (±18.3%) i/s - 15.375k in 5.014244s | 1.345k (± 5.5%) i/s - 6.834k in 5.096706s | 1.439k (±12.9%) i/s - 7.100k in 5.033603s |
156
- | | validates_uniqueness_of | 2.002k (±10.9%) i/s - 9.900k in 5.018449s | 667.100 (± 4.8%) i/s - 3.350k in 5.034451s | 606.334 (± 4.9%) i/s - 3.068k in 5.072587s |
157
- | Each hundredth item is duplicate | validates_db_uniqueness_of | 3.534k (± 5.6%) i/s - 17.748k in 5.039277s | 1.351k (± 6.5%) i/s - 6.750k in 5.017280s | 1.436k (±11.6%) i/s - 7.154k in 5.062644s |
158
- | | validates_uniqueness_of | 2.121k (± 6.8%) i/s - 10.653k in 5.049739s | 658.199 (± 6.1%) i/s - 3.350k in 5.110176s | 596.024 (± 6.7%) i/s - 2.989k in 5.041497s |
159
-
160
- ## Testing (RSpec)
161
-
162
- Add `require database_validations/rspec/matchers'` to your `spec` file.
163
-
164
- ### validate_db_uniqueness_of
165
-
166
- Example:
167
-
168
- ```ruby
169
- class User < ActiveRecord::Base
170
- validates_db_uniqueness_of :field, message: 'duplicate', where: '(some_field IS NULL)', scope: :another_field, index_name: :unique_index
171
- end
172
-
173
- describe 'validations' do
174
- subject { User }
175
-
176
- it { is_expected.to validate_db_uniqueness_of(:field).with_message('duplicate').with_where('(some_field IS NULL)').scoped_to(:another_field).with_index(:unique_index) }
177
- end
178
- ```
179
-
180
- ## Development
181
-
182
- You need to have installed and running `postgresql` and `mysql`.
183
- And for each adapter manually create a database called `database_validations_test`.
184
-
185
- After checking out the repo, run `bin/setup` to install dependencies.
186
-
187
- Then, run `rake spec` to run the tests. You can also run `bin/console` for
188
- an interactive prompt that will allow you to experiment.
189
-
190
- To install this gem onto your local machine, run `bundle exec rake install`.
191
- To release a new version, update the version number in `version.rb`, and then
192
- run `bundle exec rake release`, which will create a git tag for the version,
193
- push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
194
-
195
- ## Contributing
196
-
197
- [Bug reports](https://github.com/toptal/database_validations/issues) and [pull requests](https://github.com/toptal/database_validations/pulls) are welcome on GitHub.
198
- This project is intended to be a safe, welcoming space for collaboration, and contributors are expected
199
- to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
200
-
201
- ## License
202
-
203
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
204
-
205
- ## Code of Conduct
206
-
207
- Everyone interacting in the DatabaseValidations project’s codebases, issue trackers, chat rooms and mailing
208
- lists is expected to follow the [code of conduct](https://github.com/toptal/database_validations/blob/master/CODE_OF_CONDUCT.md).
209
-
210
- ## Authors
211
-
212
- - [Evgeniy Demin](https://github.com/djezzzl)
@@ -1,26 +0,0 @@
1
- module DatabaseValidations
2
- class UniquenessOptionsStorage
3
-
4
- def initialize(klass)
5
- @adapter = Adapters.factory(klass)
6
- @storage = {}
7
- end
8
-
9
- def push(field, options)
10
- uniqueness_options = UniquenessOptions.new(field, options, adapter)
11
- storage[uniqueness_options.key] = uniqueness_options
12
- end
13
-
14
- def [](key)
15
- storage[key]
16
- end
17
-
18
- def each_validator
19
- storage.each_value { |validator| yield(validator) }
20
- end
21
-
22
- private
23
-
24
- attr_reader :storage, :adapter
25
- end
26
- end