database_validations 0.6.0 → 0.7.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: 34f55425dd0da6279bd235d760df174ac1f028365f00d88be653b601207b526d
4
- data.tar.gz: 8ef9de8f18ba558c5c1cfb9be9af9f0257cd188df35b05545fc479490f799316
3
+ metadata.gz: 5c0e233ecf1999d511840c65bbce2744faf3af3cfc2a1c64f1bcee504c86129e
4
+ data.tar.gz: ed0cb02abcb98e1e4e8d6172a3695b953fdcc9f05561dd6c7c63945929705061
5
5
  SHA512:
6
- metadata.gz: 8ad092489894f94f52ac4ed99b83dedb383bd0643af8f938c8225c3d1f1cf69d625d2b687346492f45980851a93b45489c76043f5c98be836fcc3739d4f4e1aa
7
- data.tar.gz: ee668b15d32473eda6b01fc18ab4c9d7336dfd415ee42e33e2e27168f444a3bef7fc4067b6c59793ef5c065cccd5ddca28469439d3c53bb213bc4468e6c42bdb
6
+ metadata.gz: aa02581639a2a37ae25202d32aee451934d607a96843fe13227096eeda0631ce759e538d14e34c3e523e6302ec91fe88c3a651aabd3cda717ace5fb13f9c824e
7
+ data.tar.gz: 0f84690c7ce7fdd5a19c62c20ea211b33d8a5039254e3b08a5765eef050d69f426cbfcaa0d1cdaa15f00e07c0ff503d0535ccc194bfd031a90237d18e1df2a35
data/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # DatabaseValidations
2
2
 
3
+ [![Build Status](https://travis-ci.org/toptal/database_validations.svg?branch=master)](https://travis-ci.org/toptal/database_validations)
4
+
3
5
  ActiveRecord provides validations on app level but it won't guarantee the
4
6
  consistent. In some cases, like `validates_uniqueness_of` it executes
5
7
  additional SQL query to the database and that is not very efficient.
@@ -76,18 +78,19 @@ User.create!(email: 'email@mail.com')
76
78
 
77
79
  We want to provide full compatibility with existing `validates_uniqueness_of` validator.
78
80
 
79
- List of supported options from `validates_uniqueness_of` validator:
81
+ | Option name | PostgreSQL | MySQL | SQLite |
82
+ | -------------- | :--------: | :---: | :----: |
83
+ | scope | + | + | + |
84
+ | message | + | + | + |
85
+ | if | + | + | + |
86
+ | unless | + | + | + |
87
+ | index_name | + | + | - |
88
+ | where | + | - | - |
89
+ | case_sensitive | - | - | - |
90
+ | allow_nil | - | - | - |
91
+ | allow_blank | - | - | - |
80
92
 
81
- - `scope`: One or more columns by which to limit the scope of the uniqueness constraint.
82
- - `message`: Specifies a custom error message (default is: "has already been taken").
83
- - `if`: Specifies a method or proc to call to determine if the validation should occur
84
- (e.g. `if: :allow_validation`, or `if: Proc.new { |user| user.signup_step > 2 }`). The method or
85
- proc should return or evaluate to a `true` or `false` value.
86
- - `unless`: Specifies a method or proc to call to determine if the validation should not
87
- occur (e.g. `unless: :skip_validation`, or `unless: Proc.new { |user| user.signup_step <= 2 }`).
88
- The method or proc should return or evaluate to a `true` or `false` value.
89
-
90
- **Keep in mind**: Both `if` and `unless` options are used only for `valid?` method and provided only for performance reason.
93
+ **Keep in mind**: Both `if` and `unless` options are used only for `valid?` method and provided only for performance reason.
91
94
 
92
95
  ```ruby
93
96
  class User < ActiveRecord::Base
@@ -112,20 +115,23 @@ Is the same by default as the following
112
115
  validates_uniqueness_of :email, allow_nil: true, allow_blank: false, case_sensitive: true
113
116
  ```
114
117
 
115
- List of supported options for `PostgreSQL` only:
116
-
118
+ Options descriptions:
119
+ - `scope`: One or more columns by which to limit the scope of the uniqueness constraint.
120
+ - `message`: Specifies a custom error message (default is: "has already been taken").
121
+ - `if`: Specifies a method or proc to call to determine if the validation should occur
122
+ (e.g. `if: :allow_validation`, or `if: Proc.new { |user| user.signup_step > 2 }`). The method or
123
+ proc should return or evaluate to a `true` or `false` value.
124
+ - `unless`: Specifies a method or proc to call to determine if the validation should not
125
+ occur (e.g. `unless: :skip_validation`, or `unless: Proc.new { |user| user.signup_step <= 2 }`).
126
+ The method or proc should return or evaluate to a `true` or `false` value.
117
127
  - `where`: Specify the conditions to be included as a `WHERE` SQL fragment to
118
128
  limit the uniqueness constraint lookup (e.g. `where: "(status = 'active')"`).
119
129
  For backward compatibility, this will be converted automatically
120
130
  to `conditions: -> { where("(status = 'active')") }` for `valid?` method.
121
-
122
- The list of options to add support:
123
-
124
131
  - `case_sensitive`: Looks for an exact match. Ignored by non-text columns (`true` by default).
125
132
  - `allow_nil`: If set to `true`, skips this validation if the attribute is `nil` (default is `false`).
126
133
  - `allow_blank`: If set to `true`, skips this validation if the attribute is blank (default is `false`).
127
-
128
- **Note**: For `PosgreSQL`, it is possible to replace these options with combination of other supported options.
134
+ - `index_name`: Allows to make explicit connection between validator and index. Used when gem can't automatically find index.
129
135
 
130
136
  ### Benchmark ([code](https://github.com/toptal/database_validations/blob/master/benchmarks/uniqueness_validator_benchmark.rb))
131
137
 
@@ -8,12 +8,15 @@ module DatabaseValidations
8
8
  @model = model
9
9
  end
10
10
 
11
+ # @param [String] index_name
11
12
  def find_index_by_name(index_name)
12
13
  indexes.find { |index| index.name == index_name }
13
14
  end
14
15
 
16
+ # @param [Array<String>] columns
17
+ # @param [String] where
15
18
  def find_index(columns, where)
16
- indexes.find { |index| index.columns.map(&:to_s).sort == columns && index.where == where }
19
+ indexes.find { |index| Array.wrap(index.columns).map(&:to_s).sort == columns && index.where == where }
17
20
  end
18
21
 
19
22
  def indexes
@@ -1,12 +1,15 @@
1
1
  module DatabaseValidations
2
2
  module Adapters
3
3
  class MysqlAdapter < BaseAdapter
4
- SUPPORTED_OPTIONS = %i[scope message if unless].freeze
4
+ SUPPORTED_OPTIONS = %i[scope message if unless index_name].freeze
5
5
  ADAPTER = :mysql2
6
6
 
7
+ def index_name(error_message)
8
+ error_message[/key '([^']+)'/, 1]
9
+ end
10
+
7
11
  def error_columns(error_message)
8
- index_name = error_message[/key '([^']+)'/, 1]
9
- find_index_by_name(index_name).columns
12
+ find_index_by_name(index_name(error_message)).columns
10
13
  end
11
14
  end
12
15
  end
@@ -1,12 +1,15 @@
1
1
  module DatabaseValidations
2
2
  module Adapters
3
3
  class PostgresqlAdapter < BaseAdapter
4
- SUPPORTED_OPTIONS = %i[scope message where if unless].freeze
4
+ SUPPORTED_OPTIONS = %i[scope message where if unless index_name].freeze
5
5
  ADAPTER = :postgresql
6
6
 
7
+ def index_name(error_message)
8
+ error_message[/unique constraint "([^"]+)"/, 1]
9
+ end
10
+
7
11
  def error_columns(error_message)
8
- index_name = error_message[/unique constraint "([^"]+)"/, 1]
9
- find_index_by_name(index_name).columns
12
+ find_index_by_name(index_name(error_message)).columns
10
13
  end
11
14
  end
12
15
  end
@@ -4,6 +4,9 @@ module DatabaseValidations
4
4
  SUPPORTED_OPTIONS = %i[scope message if unless].freeze
5
5
  ADAPTER = :sqlite3
6
6
 
7
+ def index_name(_error_message)
8
+ end
9
+
7
10
  def error_columns(error_message)
8
11
  error_message.scan(/#{model.table_name}\.([^,:]+)/).flatten
9
12
  end
@@ -3,17 +3,23 @@ module DatabaseValidations
3
3
  class Base < StandardError; end
4
4
 
5
5
  class IndexNotFound < Base
6
- attr_reader :columns, :where_clause, :available_indexes
6
+ attr_reader :columns, :where_clause, :index_name, :available_indexes
7
7
 
8
- def initialize(columns, where_clause, available_indexes)
9
- @columns = columns.map(&:to_s)
8
+ def initialize(columns, where_clause, index_name, available_indexes)
9
+ @columns = columns
10
10
  @where_clause = where_clause
11
11
  @available_indexes = available_indexes
12
+ @index_name = index_name
12
13
 
13
- super "No unique index found with #{columns_and_where_text(columns, where_clause)}. "\
14
- "Available indexes are: [#{self.available_indexes.map { |ind| columns_and_where_text(ind.columns, ind.where) }.join(', ')}]. "\
15
- "Use ENV['SKIP_DB_UNIQUENESS_VALIDATOR_INDEX_CHECK']=true in case you want to skip the check. "\
16
- "For example, when you run migrations."
14
+ text = if index_name
15
+ "No unique index found with name: \"#{index_name}\". "\
16
+ "Available indexes are: #{self.available_indexes.map(&:name)}. "
17
+ else
18
+ "No unique index found with #{columns_and_where_text(columns, where_clause)}. "\
19
+ "Available indexes are: [#{self.available_indexes.map { |ind| columns_and_where_text(ind.columns, ind.where) }.join(', ')}]. "
20
+ end
21
+
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."
17
23
  end
18
24
 
19
25
  def columns_and_where_text(columns, where)
@@ -3,10 +3,13 @@ module DatabaseValidations
3
3
  module_function
4
4
 
5
5
  def handle_unique_error!(instance, error)
6
- key = generate_key(Adapters.factory(instance.class).error_columns(error.message))
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
9
 
8
10
  each_options_storage(instance.class) do |storage|
9
- return storage[key].handle_unique_error(instance) if storage[key]
11
+ return storage[index_key].handle_unique_error(instance) if storage[index_key]
12
+ return storage[column_key].handle_unique_error(instance) if storage[column_key]
10
13
  end
11
14
 
12
15
  raise error
@@ -1,5 +1,7 @@
1
1
  module DatabaseValidations
2
2
  class UniquenessOptions
3
+ CUSTOM_OPTIONS = %i[where index_name].freeze
4
+
3
5
  attr_reader :field
4
6
 
5
7
  def initialize(field, options, adapter)
@@ -12,45 +14,57 @@ module DatabaseValidations
12
14
  end
13
15
 
14
16
  def handle_unique_error(instance)
15
- error_options = options.except(:case_sensitive, :scope, :conditions, :attributes, :where)
17
+ error_options = options.except(:case_sensitive, :scope, :conditions, :attributes, *CUSTOM_OPTIONS)
16
18
  error_options[:value] = instance.public_send(options[:attributes])
17
19
 
18
20
  instance.errors.add(options[:attributes], :taken, error_options)
19
21
  end
20
22
 
23
+ # @return [Hash<Symbol, Object>]
21
24
  def validates_uniqueness_options
22
25
  where_clause_str = where_clause
23
26
 
24
- options.except(:where)
27
+ options.except(*CUSTOM_OPTIONS)
25
28
  .merge(allow_nil: true, case_sensitive: true, allow_blank: false)
26
29
  .tap { |opts| opts[:conditions] = -> { where(where_clause_str) } if where_clause }
27
30
  end
28
31
 
32
+ # @return [Boolean]
29
33
  def if_and_unless_pass?(instance)
30
34
  (options[:if].nil? || condition_passes?(options[:if], instance)) &&
31
35
  (options[:unless].nil? || !condition_passes?(options[:unless], instance))
32
36
  end
33
37
 
38
+ # @return [String]
34
39
  def key
35
- @key ||= Helpers.generate_key(columns)
40
+ @key ||= index_name ? index_name.to_s : Helpers.generate_key(columns)
36
41
  end
37
42
 
43
+ # @return [Array<String>]
38
44
  def columns
39
45
  @columns ||= Helpers.unify_columns(field, scope)
40
46
  end
41
47
 
48
+ # @return [String|nil]
42
49
  def where_clause
43
50
  @where_clause ||= options[:where]
44
51
  end
45
52
 
53
+ # @return [String|nil]
46
54
  def message
47
55
  @message ||= options[:message]
48
56
  end
49
57
 
58
+ # @return [Array<String|Symbol>]
50
59
  def scope
51
60
  @scope ||= Array.wrap(options[:scope])
52
61
  end
53
62
 
63
+ # @return [String|Symbol|nil]
64
+ def index_name
65
+ @index_name ||= options[:index_name]
66
+ end
67
+
54
68
  private
55
69
 
56
70
  attr_reader :adapter, :options
@@ -74,8 +88,10 @@ module DatabaseValidations
74
88
  end
75
89
 
76
90
  def raise_if_index_missed!
77
- raise Errors::IndexNotFound.new(columns, where_clause, adapter.indexes) unless adapter.find_index(columns, where_clause)
91
+ unless (index_name && adapter.find_index_by_name(index_name.to_s)) ||
92
+ (!index_name && adapter.find_index(columns, where_clause))
93
+ raise Errors::IndexNotFound.new(columns, where_clause, index_name, adapter.indexes)
94
+ end
78
95
  end
79
96
  end
80
97
  end
81
-
@@ -1,3 +1,3 @@
1
1
  module DatabaseValidations
2
- VERSION = '0.6.0'
2
+ VERSION = '0.7.0'
3
3
  end
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.6.0
4
+ version: 0.7.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-09-26 00:00:00.000000000 Z
11
+ date: 2018-10-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord