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 +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
|
+
[](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
|