database_validations 0.7.3 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
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