database_validations 0.7.3 → 0.8.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/lib/database_validations/rspec/uniqueness_validator_matcher.rb +1 -1
- data/lib/database_validations/validations/adapters/base_adapter.rb +8 -0
- data/lib/database_validations/validations/adapters/mysql_adapter.rb +5 -1
- data/lib/database_validations/validations/adapters/postgresql_adapter.rb +5 -1
- data/lib/database_validations/validations/adapters/sqlite_adapter.rb +9 -1
- data/lib/database_validations/validations/belongs_to_handlers.rb +58 -0
- data/lib/database_validations/validations/belongs_to_options.rb +43 -0
- data/lib/database_validations/validations/errors.rb +27 -2
- data/lib/database_validations/validations/helpers.rb +45 -7
- data/lib/database_validations/validations/options_storage.rb +35 -0
- data/lib/database_validations/validations/{uniqueness_validator.rb → uniqueness_handlers.rb} +12 -13
- data/lib/database_validations/validations/uniqueness_options.rb +1 -1
- data/lib/database_validations/validations/valid_without_database_validations.rb +9 -0
- data/lib/database_validations/version.rb +1 -1
- data/lib/database_validations.rb +7 -2
- metadata +8 -7
- data/LICENSE.txt +0 -21
- data/README.md +0 -212
- data/lib/database_validations/validations/uniqueness_options_storage.rb +0 -26
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 047eabab521a0b04f0385213e7b20a34cb192576935d3e9c2f70aeab28df6ca0
|
4
|
+
data.tar.gz: 61fe8cac30624557d22ff059b2738ccdddbf0bbaa99eb9ac67a74f943fe39abf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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
|
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
|
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
|
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
|
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 +
|
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 =
|
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(:'@
|
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
|
46
|
+
def each_uniqueness_validator(klass)
|
27
47
|
each_options_storage(klass) do |storage|
|
28
|
-
storage.
|
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
|
37
|
-
|
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
|
data/lib/database_validations/validations/{uniqueness_validator.rb → uniqueness_handlers.rb}
RENAMED
@@ -3,21 +3,19 @@ module DatabaseValidations
|
|
3
3
|
extend ActiveSupport::Concern
|
4
4
|
|
5
5
|
included do
|
6
|
-
alias_method :
|
6
|
+
alias_method :validate, :valid?
|
7
|
+
end
|
7
8
|
|
8
|
-
|
9
|
-
|
9
|
+
def valid?(context = nil)
|
10
|
+
output = super(context)
|
10
11
|
|
11
|
-
|
12
|
-
|
13
|
-
|
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
|
-
|
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 ||
|
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
|
-
|
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
|
-
@
|
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
|
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>]
|
data/lib/database_validations.rb
CHANGED
@@ -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/
|
7
|
+
require 'database_validations/validations/uniqueness_handlers'
|
8
8
|
require 'database_validations/validations/uniqueness_options'
|
9
|
-
|
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.
|
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-
|
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/
|
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.
|
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
|
-
[](https://travis-ci.org/toptal/database_validations)
|
4
|
-
[](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
|