database_validations 0.6.0 → 0.7.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: 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