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 +4 -4
- data/README.md +24 -18
- data/lib/database_validations/validations/adapters/base_adapter.rb +4 -1
- data/lib/database_validations/validations/adapters/mysql_adapter.rb +6 -3
- data/lib/database_validations/validations/adapters/postgresql_adapter.rb +6 -3
- data/lib/database_validations/validations/adapters/sqlite_adapter.rb +3 -0
- data/lib/database_validations/validations/errors.rb +13 -7
- data/lib/database_validations/validations/helpers.rb +5 -2
- data/lib/database_validations/validations/uniqueness_options.rb +21 -5
- data/lib/database_validations/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5c0e233ecf1999d511840c65bbce2744faf3af3cfc2a1c64f1bcee504c86129e
|
4
|
+
data.tar.gz: ed0cb02abcb98e1e4e8d6172a3695b953fdcc9f05561dd6c7c63945929705061
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
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
|
@@ -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
|
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
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
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
|
-
|
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[
|
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,
|
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(
|
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
|
-
|
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
|
-
|
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.
|
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-
|
11
|
+
date: 2018-10-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|