database_validations 0.8.10 → 0.9.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.rb +12 -9
- data/lib/database_validations/lib/adapters/base_adapter.rb +0 -16
- data/lib/database_validations/lib/adapters/mysql_adapter.rb +0 -1
- data/lib/database_validations/lib/adapters/postgresql_adapter.rb +0 -1
- data/lib/database_validations/lib/adapters/sqlite_adapter.rb +0 -1
- data/lib/database_validations/lib/attribute_validator.rb +3 -0
- data/lib/database_validations/lib/checkers/db_presence_validator.rb +36 -0
- data/lib/database_validations/lib/checkers/db_uniqueness_validator.rb +47 -0
- data/lib/database_validations/lib/errors.rb +0 -11
- data/lib/database_validations/lib/injector.rb +13 -0
- data/lib/database_validations/lib/key_generator.rb +32 -0
- data/lib/database_validations/lib/presence_key_extractor.rb +18 -0
- data/lib/database_validations/lib/rescuer.rb +24 -19
- data/lib/database_validations/lib/storage.rb +28 -0
- data/lib/database_validations/lib/uniqueness_key_extractor.rb +38 -0
- data/lib/database_validations/lib/validations.rb +31 -0
- data/lib/database_validations/lib/validators/db_presence_validator.rb +63 -0
- data/lib/database_validations/lib/validators/db_uniqueness_validator.rb +48 -0
- data/lib/database_validations/rspec/uniqueness_validator_matcher.rb +10 -8
- data/lib/database_validations/version.rb +1 -1
- metadata +34 -17
- data/lib/database_validations/lib/db_belongs_to/belongs_to_handlers.rb +0 -30
- data/lib/database_validations/lib/db_belongs_to/belongs_to_options.rb +0 -46
- data/lib/database_validations/lib/db_belongs_to/db_presence_validator.rb +0 -30
- data/lib/database_validations/lib/helpers.rb +0 -79
- data/lib/database_validations/lib/options_storage.rb +0 -31
- data/lib/database_validations/lib/validates_db_uniqueness_of/db_uniqueness_validator.rb +0 -14
- data/lib/database_validations/lib/validates_db_uniqueness_of/uniqueness_handlers.rb +0 -20
- data/lib/database_validations/lib/validates_db_uniqueness_of/uniqueness_options.rb +0 -108
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 70578fbf45115b1b0a349f2676b2c1a689f2aa8d29da2da189da263306c0b333
|
|
4
|
+
data.tar.gz: f6fc66d339e488cccdf750103637543ecd39d34a0a84366efa2382addcde6aee
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6d40cf1b7d776cf984846f9cef85f1969271d619ae4ee9b9127fe67307c8983eb0ce385883541394932d9cc1f88a2e69fccdf5ff6b800ee06c5a1f5710b21495
|
|
7
|
+
data.tar.gz: 1e94c35ae4a40a56e275b6bad79a05cf0c3f29b735cf37495c2fb5e9013d2929ab0a54f025c6221a5fa8179a26d3660c9518ba8d5c1f4310ea3832b4ab4cdb28
|
data/lib/database_validations.rb
CHANGED
|
@@ -4,18 +4,21 @@ require 'database_validations/version'
|
|
|
4
4
|
|
|
5
5
|
require 'database_validations/rails/railtie' if defined?(Rails)
|
|
6
6
|
|
|
7
|
-
require 'database_validations/lib/
|
|
8
|
-
require 'database_validations/lib/
|
|
9
|
-
require 'database_validations/lib/validates_db_uniqueness_of/uniqueness_options'
|
|
7
|
+
require 'database_validations/lib/checkers/db_uniqueness_validator'
|
|
8
|
+
require 'database_validations/lib/checkers/db_presence_validator'
|
|
10
9
|
|
|
11
|
-
require 'database_validations/lib/
|
|
12
|
-
require 'database_validations/lib/
|
|
13
|
-
require 'database_validations/lib/db_belongs_to/belongs_to_options'
|
|
10
|
+
require 'database_validations/lib/validators/db_uniqueness_validator'
|
|
11
|
+
require 'database_validations/lib/validators/db_presence_validator'
|
|
14
12
|
|
|
15
|
-
require 'database_validations/lib/
|
|
16
|
-
require 'database_validations/lib/
|
|
13
|
+
require 'database_validations/lib/storage'
|
|
14
|
+
require 'database_validations/lib/attribute_validator'
|
|
15
|
+
require 'database_validations/lib/key_generator'
|
|
16
|
+
require 'database_validations/lib/uniqueness_key_extractor'
|
|
17
|
+
require 'database_validations/lib/presence_key_extractor'
|
|
18
|
+
require 'database_validations/lib/validations'
|
|
17
19
|
require 'database_validations/lib/errors'
|
|
18
|
-
require 'database_validations/lib/
|
|
20
|
+
require 'database_validations/lib/rescuer'
|
|
21
|
+
require 'database_validations/lib/injector'
|
|
19
22
|
require 'database_validations/lib/adapters'
|
|
20
23
|
|
|
21
24
|
module DatabaseValidations
|
|
@@ -31,22 +31,6 @@ module DatabaseValidations
|
|
|
31
31
|
foreign_keys.find { |foreign_key| foreign_key.column.to_s == column.to_s }
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
-
# @return [Symbol]
|
|
35
|
-
def adapter_name
|
|
36
|
-
self.class::ADAPTER
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
# @param [Symbol] option_name
|
|
40
|
-
# @return [Boolean]
|
|
41
|
-
def support_option?(option_name)
|
|
42
|
-
supported_options.include?(option_name.to_sym)
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
# @return [Array<Symbol>]
|
|
46
|
-
def supported_options
|
|
47
|
-
self.class::SUPPORTED_OPTIONS
|
|
48
|
-
end
|
|
49
|
-
|
|
50
34
|
# @return [String]
|
|
51
35
|
def table_name
|
|
52
36
|
model.table_name
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module DatabaseValidations
|
|
2
|
+
module Checkers
|
|
3
|
+
class DbPresenceValidator
|
|
4
|
+
attr_reader :validator
|
|
5
|
+
|
|
6
|
+
# @param [DatabaseValidations::DbPresenceValidator]
|
|
7
|
+
def self.validate!(validator)
|
|
8
|
+
new(validator).validate!
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# @param [DatabaseValidations::DbPresenceValidator]
|
|
12
|
+
def initialize(validator)
|
|
13
|
+
@validator = validator
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def validate!
|
|
17
|
+
validate_foreign_keys! unless ENV['SKIP_DB_UNIQUENESS_VALIDATOR_INDEX_CHECK']
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def validate_foreign_keys! # rubocop:disable Metrics/AbcSize
|
|
23
|
+
adapter = Adapters::BaseAdapter.new(validator.klass)
|
|
24
|
+
|
|
25
|
+
validator.attributes.each do |attribute|
|
|
26
|
+
reflection = validator.klass._reflect_on_association(attribute)
|
|
27
|
+
|
|
28
|
+
next unless reflection
|
|
29
|
+
next if adapter.find_foreign_key_by_column(reflection.foreign_key)
|
|
30
|
+
|
|
31
|
+
raise Errors::ForeignKeyNotFound.new(reflection.foreign_key, adapter.foreign_keys)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
module DatabaseValidations
|
|
2
|
+
module Checkers
|
|
3
|
+
class DbUniquenessValidator
|
|
4
|
+
attr_reader :validator
|
|
5
|
+
|
|
6
|
+
# @param [DatabaseValidations::DbUniquenessValidator]
|
|
7
|
+
def self.validate!(validator)
|
|
8
|
+
new(validator).validate!
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# @param [DatabaseValidations::DbUniquenessValidator]
|
|
12
|
+
def initialize(validator)
|
|
13
|
+
@validator = validator
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def validate!
|
|
17
|
+
validate_index_usage!
|
|
18
|
+
|
|
19
|
+
validate_indexes! unless ENV['SKIP_DB_UNIQUENESS_VALIDATOR_INDEX_CHECK']
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def valid_index?(columns, index)
|
|
25
|
+
index_columns_size = index.columns.is_a?(Array) ? index.columns.size : (index.columns.count(',') + 1)
|
|
26
|
+
|
|
27
|
+
(columns.size == index_columns_size) && (validator.where.nil? == index.where.nil?)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def validate_index_usage!
|
|
31
|
+
return unless validator.index_name.present? && validator.attributes.size > 1
|
|
32
|
+
|
|
33
|
+
raise ArgumentError, "When index_name is provided validator can have only one attribute. See #{validator.inspect}"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def validate_indexes! # rubocop:disable Metrics/AbcSize
|
|
37
|
+
adapter = Adapters::BaseAdapter.new(validator.klass)
|
|
38
|
+
|
|
39
|
+
validator.attributes.map do |attribute|
|
|
40
|
+
columns = KeyGenerator.unify_columns(attribute, validator.options[:scope])
|
|
41
|
+
index = validator.index_name ? adapter.find_index_by_name(validator.index_name.to_s) : adapter.find_index(columns, validator.where) # rubocop:disable Metrics/LineLength
|
|
42
|
+
raise Errors::IndexNotFound.new(columns, validator.where, validator.index_name, adapter.indexes, adapter.table_name) unless index && valid_index?(columns, index) # rubocop:disable Metrics/LineLength
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -41,17 +41,6 @@ module DatabaseValidations
|
|
|
41
41
|
end
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
-
class OptionIsNotSupported < Base
|
|
45
|
-
attr_reader :option, :database, :supported_options
|
|
46
|
-
|
|
47
|
-
def initialize(option, database, supported_options)
|
|
48
|
-
@option = option
|
|
49
|
-
@database = database
|
|
50
|
-
@supported_options = supported_options
|
|
51
|
-
super "Option #{self.option} is not supported for #{self.database}. Supported options are: #{self.supported_options}"
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
|
|
55
44
|
class ForeignKeyNotFound < Base
|
|
56
45
|
attr_reader :column, :foreign_keys
|
|
57
46
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module DatabaseValidations
|
|
2
|
+
module Injector
|
|
3
|
+
module_function
|
|
4
|
+
|
|
5
|
+
# @param [ActiveRecord::Base] model
|
|
6
|
+
def inject(model)
|
|
7
|
+
return if model.method_defined?(:valid_without_database_validations?)
|
|
8
|
+
|
|
9
|
+
model.__send__(:alias_method, :valid_without_database_validations?, :valid?)
|
|
10
|
+
model.include(DatabaseValidations::Validations)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module DatabaseValidations
|
|
2
|
+
module KeyGenerator
|
|
3
|
+
module_function
|
|
4
|
+
|
|
5
|
+
# @param [String] index_name
|
|
6
|
+
#
|
|
7
|
+
# @return [String]
|
|
8
|
+
def for_unique_index(index_name)
|
|
9
|
+
generate_key(:unique_index, index_name)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# @return [String]
|
|
13
|
+
def for_db_uniqueness(*columns)
|
|
14
|
+
generate_key(:db_uniqueness, columns)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# @return [String]
|
|
18
|
+
def for_db_presence(column)
|
|
19
|
+
generate_key(:db_presence, column)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @return [String]
|
|
23
|
+
def generate_key(type, *args)
|
|
24
|
+
[type, *unify_columns(args)].join('__')
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @return [String]
|
|
28
|
+
def unify_columns(*args)
|
|
29
|
+
args.flatten.compact.map(&:to_s).sort
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module DatabaseValidations
|
|
2
|
+
module PresenceKeyExtractor
|
|
3
|
+
module_function
|
|
4
|
+
|
|
5
|
+
# @param [DatabaseValidations::DbPresenceValidator]
|
|
6
|
+
#
|
|
7
|
+
# @return [Hash]
|
|
8
|
+
def attribute_by_key(validator)
|
|
9
|
+
validator.attributes.map do |attribute|
|
|
10
|
+
reflection = validator.klass._reflect_on_association(attribute)
|
|
11
|
+
|
|
12
|
+
key = reflection ? reflection.foreign_key : attribute
|
|
13
|
+
|
|
14
|
+
[KeyGenerator.for_db_presence(key), attribute]
|
|
15
|
+
end.to_h
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -1,31 +1,36 @@
|
|
|
1
1
|
module DatabaseValidations
|
|
2
2
|
module Rescuer
|
|
3
|
-
|
|
3
|
+
module_function
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
end
|
|
8
|
-
|
|
9
|
-
attr_accessor :_database_validations_fallback
|
|
5
|
+
def handled?(instance, error)
|
|
6
|
+
Storage.prepare(instance.class) unless Storage.prepared?(instance.class)
|
|
10
7
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
case error
|
|
9
|
+
when ActiveRecord::RecordNotUnique
|
|
10
|
+
process(instance, error, for_unique_index: :unique_index_name, for_db_uniqueness: :unique_error_columns)
|
|
11
|
+
when ActiveRecord::InvalidForeignKey
|
|
12
|
+
process(instance, error, for_db_presence: :foreign_key_error_column)
|
|
13
|
+
else false
|
|
14
|
+
end
|
|
14
15
|
end
|
|
15
16
|
|
|
16
|
-
def
|
|
17
|
-
|
|
18
|
-
ActiveRecord::Base.connection.transaction(requires_new: true) { super }
|
|
19
|
-
rescue ActiveRecord::InvalidForeignKey, ActiveRecord::RecordNotUnique => error
|
|
20
|
-
raise error unless Helpers.handle_error!(self, error)
|
|
17
|
+
def process(instance, error, key_types)
|
|
18
|
+
adapter = Adapters.factory(instance.class)
|
|
21
19
|
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
keys = key_types.map do |key_generator, error_processor|
|
|
21
|
+
KeyGenerator.public_send(key_generator, adapter.public_send(error_processor, error.message))
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
keys.each do |key|
|
|
25
|
+
attribute_validator = instance._db_validators[key]
|
|
24
26
|
|
|
25
|
-
|
|
27
|
+
if attribute_validator
|
|
28
|
+
attribute_validator.validator.apply_error(instance, attribute_validator.attribute)
|
|
29
|
+
return true
|
|
30
|
+
end
|
|
31
|
+
end
|
|
26
32
|
|
|
27
|
-
|
|
28
|
-
options[:validate] == false || valid_without_database_validations?(options[:context])
|
|
33
|
+
false
|
|
29
34
|
end
|
|
30
35
|
end
|
|
31
36
|
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module DatabaseValidations
|
|
2
|
+
module Storage
|
|
3
|
+
module_function
|
|
4
|
+
|
|
5
|
+
def prepare(model)
|
|
6
|
+
model.class_attribute :_db_validators, instance_writer: false
|
|
7
|
+
model._db_validators = {}
|
|
8
|
+
|
|
9
|
+
model.validators.each do |validator|
|
|
10
|
+
case validator
|
|
11
|
+
when DbUniquenessValidator then process(validator, UniquenessKeyExtractor, model)
|
|
12
|
+
when DbPresenceValidator then process(validator, PresenceKeyExtractor, model)
|
|
13
|
+
else next
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def process(validator, extractor, model)
|
|
19
|
+
extractor.attribute_by_key(validator).each do |key, attribute|
|
|
20
|
+
model._db_validators[key] = AttributeValidator.new(attribute, validator)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def prepared?(model)
|
|
25
|
+
model.respond_to?(:_db_validators)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module DatabaseValidations
|
|
2
|
+
module UniquenessKeyExtractor
|
|
3
|
+
module_function
|
|
4
|
+
|
|
5
|
+
# @param [DatabaseValidations::DbUniquenessValidator]
|
|
6
|
+
#
|
|
7
|
+
# @return [Hash]
|
|
8
|
+
def attribute_by_columns_keys(validator)
|
|
9
|
+
validator.attributes.map do |attribute|
|
|
10
|
+
[KeyGenerator.for_db_uniqueness(attribute, Array.wrap(validator.options[:scope])), attribute]
|
|
11
|
+
end.to_h
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# @param [DatabaseValidations::DbUniquenessValidator]
|
|
15
|
+
#
|
|
16
|
+
# @return [Hash]
|
|
17
|
+
def attribute_by_indexes_keys(validator) # rubocop:disable Metrics/AbcSize
|
|
18
|
+
adapter = Adapters::BaseAdapter.new(validator.klass)
|
|
19
|
+
|
|
20
|
+
if validator.index_name
|
|
21
|
+
[[KeyGenerator.for_unique_index(validator.index_name), validator.attributes[0]]].to_h
|
|
22
|
+
else
|
|
23
|
+
validator.attributes.map do |attribute|
|
|
24
|
+
columns = KeyGenerator.unify_columns(attribute, validator.options[:scope])
|
|
25
|
+
index = adapter.find_index(columns, validator.where)
|
|
26
|
+
[KeyGenerator.for_unique_index(index.name), attribute]
|
|
27
|
+
end.to_h
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @param [DatabaseValidations::DbUniquenessValidator]
|
|
32
|
+
#
|
|
33
|
+
# @return [Hash]
|
|
34
|
+
def attribute_by_key(validator)
|
|
35
|
+
attribute_by_columns_keys(validator).merge(attribute_by_indexes_keys(validator))
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module DatabaseValidations
|
|
2
|
+
module Validations
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
included do
|
|
6
|
+
alias_method :validate, :valid?
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
attr_accessor :_database_validations_fallback
|
|
10
|
+
|
|
11
|
+
def valid?(context = nil)
|
|
12
|
+
self._database_validations_fallback = true
|
|
13
|
+
super(context)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def create_or_update(*args, &block)
|
|
17
|
+
self._database_validations_fallback = false
|
|
18
|
+
ActiveRecord::Base.connection.transaction(requires_new: true) { super }
|
|
19
|
+
rescue ActiveRecord::InvalidForeignKey, ActiveRecord::RecordNotUnique => e
|
|
20
|
+
raise e unless Rescuer.handled?(self, e)
|
|
21
|
+
|
|
22
|
+
raise ActiveRecord::RecordInvalid, self
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def perform_validations(options = {})
|
|
28
|
+
options[:validate] == false || valid_without_database_validations?(options[:context])
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
module DatabaseValidations
|
|
2
|
+
class DbPresenceValidator < ActiveRecord::Validations::PresenceValidator
|
|
3
|
+
REFLECTION_MESSAGE = ActiveRecord::VERSION::MAJOR < 5 ? :blank : :required
|
|
4
|
+
|
|
5
|
+
attr_reader :klass
|
|
6
|
+
|
|
7
|
+
# Used to make 3rd party libraries work correctly
|
|
8
|
+
#
|
|
9
|
+
# @return [Symbol]
|
|
10
|
+
def self.kind
|
|
11
|
+
:presence
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# @param [Hash] options
|
|
15
|
+
def initialize(options)
|
|
16
|
+
@klass = options[:class]
|
|
17
|
+
|
|
18
|
+
super
|
|
19
|
+
|
|
20
|
+
Injector.inject(klass)
|
|
21
|
+
Checkers::DbPresenceValidator.validate!(self)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# TODO: add support of optional db_belongs_to
|
|
25
|
+
def validate(record)
|
|
26
|
+
if record._database_validations_fallback
|
|
27
|
+
super
|
|
28
|
+
else
|
|
29
|
+
attributes.each do |attribute|
|
|
30
|
+
reflection = record.class._reflect_on_association(attribute)
|
|
31
|
+
|
|
32
|
+
next if reflection && record.public_send(reflection.foreign_key).present?
|
|
33
|
+
|
|
34
|
+
validate_each(record, attribute, record.public_send(attribute))
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def apply_error(instance, attribute)
|
|
40
|
+
# Helps to avoid querying the database when attribute is association
|
|
41
|
+
instance.send("#{attribute}=", nil)
|
|
42
|
+
instance.errors.add(attribute, :blank, message: REFLECTION_MESSAGE)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
module ClassMethods
|
|
47
|
+
def validates_db_presence_of(*attr_names)
|
|
48
|
+
validates_with(DatabaseValidations::DbPresenceValidator, _merge_attributes(attr_names))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def db_belongs_to(name, scope = nil, **options)
|
|
52
|
+
if ActiveRecord::VERSION::MAJOR < 5
|
|
53
|
+
options[:required] = false
|
|
54
|
+
else
|
|
55
|
+
options[:optional] = true
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
belongs_to(name, scope, options)
|
|
59
|
+
|
|
60
|
+
validates_with DatabaseValidations::DbPresenceValidator, _merge_attributes([name, message: DatabaseValidations::DbPresenceValidator::REFLECTION_MESSAGE]) # rubocop:disable Metrics/LineLength
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
module DatabaseValidations
|
|
2
|
+
class DbUniquenessValidator < ActiveRecord::Validations::UniquenessValidator
|
|
3
|
+
attr_reader :index_name, :where, :klass
|
|
4
|
+
|
|
5
|
+
# Used to make 3rd party libraries work correctly
|
|
6
|
+
#
|
|
7
|
+
# @return [Symbol]
|
|
8
|
+
def self.kind
|
|
9
|
+
:uniqueness
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# @param [Hash] options
|
|
13
|
+
def initialize(options)
|
|
14
|
+
options[:allow_nil] = true
|
|
15
|
+
options[:allow_blank] = false
|
|
16
|
+
|
|
17
|
+
if options.key?(:where)
|
|
18
|
+
condition = options[:where]
|
|
19
|
+
options[:conditions] = -> { where(condition) }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
@index_name = options.delete(:index_name) if options.key?(:index_name)
|
|
23
|
+
@where = options.delete(:where) if options.key?(:where)
|
|
24
|
+
|
|
25
|
+
super
|
|
26
|
+
|
|
27
|
+
Injector.inject(klass)
|
|
28
|
+
Checkers::DbUniquenessValidator.validate!(self)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def validate(record)
|
|
32
|
+
super if record._database_validations_fallback
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def apply_error(instance, attribute)
|
|
36
|
+
error_options = options.except(:case_sensitive, :scope, :conditions)
|
|
37
|
+
error_options[:value] = instance.public_send(attribute)
|
|
38
|
+
|
|
39
|
+
instance.errors.add(attribute, :taken, error_options)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
module ClassMethods
|
|
44
|
+
def validates_db_uniqueness_of(*attr_names)
|
|
45
|
+
validates_with(DatabaseValidations::DbUniquenessValidator, _merge_attributes(attr_names))
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -46,26 +46,28 @@ RSpec::Matchers.define :validate_db_uniqueness_of do |field| # rubocop:disable M
|
|
|
46
46
|
|
|
47
47
|
model = object.is_a?(Class) ? object : object.class
|
|
48
48
|
|
|
49
|
-
DatabaseValidations::
|
|
50
|
-
|
|
49
|
+
model.validators.grep(DatabaseValidations::DbUniquenessValidator).each do |validator|
|
|
50
|
+
validator.attributes.each do |attribute|
|
|
51
51
|
@validators << {
|
|
52
|
-
field:
|
|
53
|
-
scope: validator.scope,
|
|
54
|
-
where: validator.
|
|
55
|
-
message: validator.message,
|
|
52
|
+
field: attribute,
|
|
53
|
+
scope: Array.wrap(validator.options[:scope]),
|
|
54
|
+
where: validator.where,
|
|
55
|
+
message: validator.options[:message],
|
|
56
56
|
index_name: validator.index_name,
|
|
57
|
-
case_sensitive: validator.case_sensitive
|
|
57
|
+
case_sensitive: validator.options[:case_sensitive]
|
|
58
58
|
}
|
|
59
59
|
end
|
|
60
60
|
end
|
|
61
61
|
|
|
62
|
+
case_sensitive_default = ActiveRecord::VERSION::MAJOR >= 6 ? nil : true
|
|
63
|
+
|
|
62
64
|
@validators.include?(
|
|
63
65
|
field: field,
|
|
64
66
|
scope: Array.wrap(@scope),
|
|
65
67
|
where: @where,
|
|
66
68
|
message: @message,
|
|
67
69
|
index_name: @index_name,
|
|
68
|
-
case_sensitive: @case_sensitive
|
|
70
|
+
case_sensitive: @case_sensitive.nil? ? case_sensitive_default : @case_sensitive
|
|
69
71
|
)
|
|
70
72
|
end
|
|
71
73
|
|
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.9.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: 2019-
|
|
11
|
+
date: 2019-08-18 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activerecord
|
|
@@ -44,14 +44,28 @@ dependencies:
|
|
|
44
44
|
requirements:
|
|
45
45
|
- - "~>"
|
|
46
46
|
- !ruby/object:Gem::Version
|
|
47
|
-
version: '
|
|
47
|
+
version: '2.0'
|
|
48
48
|
type: :development
|
|
49
49
|
prerelease: false
|
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
|
51
51
|
requirements:
|
|
52
52
|
- - "~>"
|
|
53
53
|
- !ruby/object:Gem::Version
|
|
54
|
-
version: '
|
|
54
|
+
version: '2.0'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: db-query-matchers
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - ">="
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '0.9'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - ">="
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '0.9'
|
|
55
69
|
- !ruby/object:Gem::Dependency
|
|
56
70
|
name: mysql2
|
|
57
71
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -86,14 +100,14 @@ dependencies:
|
|
|
86
100
|
requirements:
|
|
87
101
|
- - "~>"
|
|
88
102
|
- !ruby/object:Gem::Version
|
|
89
|
-
version: '
|
|
103
|
+
version: '12.3'
|
|
90
104
|
type: :development
|
|
91
105
|
prerelease: false
|
|
92
106
|
version_requirements: !ruby/object:Gem::Requirement
|
|
93
107
|
requirements:
|
|
94
108
|
- - "~>"
|
|
95
109
|
- !ruby/object:Gem::Version
|
|
96
|
-
version: '
|
|
110
|
+
version: '12.3'
|
|
97
111
|
- !ruby/object:Gem::Dependency
|
|
98
112
|
name: rspec
|
|
99
113
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -142,14 +156,14 @@ dependencies:
|
|
|
142
156
|
requirements:
|
|
143
157
|
- - "~>"
|
|
144
158
|
- !ruby/object:Gem::Version
|
|
145
|
-
version: 1.3
|
|
159
|
+
version: '1.3'
|
|
146
160
|
type: :development
|
|
147
161
|
prerelease: false
|
|
148
162
|
version_requirements: !ruby/object:Gem::Requirement
|
|
149
163
|
requirements:
|
|
150
164
|
- - "~>"
|
|
151
165
|
- !ruby/object:Gem::Version
|
|
152
|
-
version: 1.3
|
|
166
|
+
version: '1.3'
|
|
153
167
|
description: |-
|
|
154
168
|
ActiveRecord provides validations on app level but it won't guarantee the
|
|
155
169
|
consistent. In some cases, like `validates_uniqueness_of` it executes
|
|
@@ -169,16 +183,19 @@ files:
|
|
|
169
183
|
- lib/database_validations/lib/adapters/mysql_adapter.rb
|
|
170
184
|
- lib/database_validations/lib/adapters/postgresql_adapter.rb
|
|
171
185
|
- lib/database_validations/lib/adapters/sqlite_adapter.rb
|
|
172
|
-
- lib/database_validations/lib/
|
|
173
|
-
- lib/database_validations/lib/
|
|
174
|
-
- lib/database_validations/lib/
|
|
186
|
+
- lib/database_validations/lib/attribute_validator.rb
|
|
187
|
+
- lib/database_validations/lib/checkers/db_presence_validator.rb
|
|
188
|
+
- lib/database_validations/lib/checkers/db_uniqueness_validator.rb
|
|
175
189
|
- lib/database_validations/lib/errors.rb
|
|
176
|
-
- lib/database_validations/lib/
|
|
177
|
-
- lib/database_validations/lib/
|
|
190
|
+
- lib/database_validations/lib/injector.rb
|
|
191
|
+
- lib/database_validations/lib/key_generator.rb
|
|
192
|
+
- lib/database_validations/lib/presence_key_extractor.rb
|
|
178
193
|
- lib/database_validations/lib/rescuer.rb
|
|
179
|
-
- lib/database_validations/lib/
|
|
180
|
-
- lib/database_validations/lib/
|
|
181
|
-
- lib/database_validations/lib/
|
|
194
|
+
- lib/database_validations/lib/storage.rb
|
|
195
|
+
- lib/database_validations/lib/uniqueness_key_extractor.rb
|
|
196
|
+
- lib/database_validations/lib/validations.rb
|
|
197
|
+
- lib/database_validations/lib/validators/db_presence_validator.rb
|
|
198
|
+
- lib/database_validations/lib/validators/db_uniqueness_validator.rb
|
|
182
199
|
- lib/database_validations/rails/railtie.rb
|
|
183
200
|
- lib/database_validations/rspec/matchers.rb
|
|
184
201
|
- lib/database_validations/rspec/uniqueness_validator_matcher.rb
|
|
@@ -207,7 +224,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
207
224
|
version: '0'
|
|
208
225
|
requirements: []
|
|
209
226
|
rubyforge_project:
|
|
210
|
-
rubygems_version: 2.7.
|
|
227
|
+
rubygems_version: 2.7.9
|
|
211
228
|
signing_key:
|
|
212
229
|
specification_version: 4
|
|
213
230
|
summary: Provide compatibility between database constraints and ActiveRecord validations
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
module DatabaseValidations
|
|
2
|
-
module ClassMethods
|
|
3
|
-
# rubocop: disable Metrics/AbcSize
|
|
4
|
-
def db_belongs_to(name, scope = nil, **options)
|
|
5
|
-
Helpers.cache_valid_method!(self)
|
|
6
|
-
|
|
7
|
-
@database_validations_storage ||= DatabaseValidations::OptionsStorage.new(self)
|
|
8
|
-
|
|
9
|
-
belongs_to_options =
|
|
10
|
-
if ActiveRecord::VERSION::MAJOR < 5
|
|
11
|
-
options.delete(:optional)
|
|
12
|
-
options.merge(required: false)
|
|
13
|
-
else
|
|
14
|
-
options.merge(optional: true)
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
belongs_to(name, scope, belongs_to_options)
|
|
18
|
-
|
|
19
|
-
foreign_key = reflections[name.to_s].foreign_key
|
|
20
|
-
|
|
21
|
-
@database_validations_storage.push_belongs_to(foreign_key, name)
|
|
22
|
-
|
|
23
|
-
validates_with DatabaseValidations::DBPresenceValidator,
|
|
24
|
-
DatabaseValidations::BelongsToOptions.validator_options(name, foreign_key)
|
|
25
|
-
|
|
26
|
-
include(DatabaseValidations::Rescuer)
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
# rubocop: enable Metrics/AbcSize
|
|
30
|
-
end
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
module DatabaseValidations
|
|
2
|
-
class BelongsToOptions
|
|
3
|
-
VALIDATOR_MESSAGE =
|
|
4
|
-
if ActiveRecord::VERSION::MAJOR < 5
|
|
5
|
-
:blank
|
|
6
|
-
else
|
|
7
|
-
:required
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
def self.validator_options(association, foreign_key)
|
|
11
|
-
{ attributes: association, foreign_key: foreign_key, message: VALIDATOR_MESSAGE }
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
attr_reader :column, :adapter, :relation
|
|
15
|
-
|
|
16
|
-
def initialize(column, relation, adapter)
|
|
17
|
-
@column = column
|
|
18
|
-
@relation = relation
|
|
19
|
-
@adapter = adapter
|
|
20
|
-
|
|
21
|
-
raise_if_unsupported_database!
|
|
22
|
-
raise_if_foreign_key_missed! unless ENV['SKIP_DB_UNIQUENESS_VALIDATOR_INDEX_CHECK']
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
# @return [String]
|
|
26
|
-
def key
|
|
27
|
-
@key ||= Helpers.generate_key_for_belongs_to(column)
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def handle_foreign_key_error(instance)
|
|
31
|
-
# Hack to not query the database because we know the result already
|
|
32
|
-
instance.send("#{relation}=", nil)
|
|
33
|
-
instance.errors.add(relation, :blank, message: VALIDATOR_MESSAGE)
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
private
|
|
37
|
-
|
|
38
|
-
def raise_if_foreign_key_missed!
|
|
39
|
-
raise Errors::ForeignKeyNotFound.new(column, adapter.foreign_keys) unless adapter.find_foreign_key_by_column(column)
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def raise_if_unsupported_database!
|
|
43
|
-
raise Errors::UnsupportedDatabase.new(:db_belongs_to, adapter.adapter_name) if adapter.adapter_name == :sqlite3
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
end
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
module DatabaseValidations
|
|
2
|
-
class DBPresenceValidator < ActiveRecord::Validations::PresenceValidator
|
|
3
|
-
# This is a hack to simulate presence validator
|
|
4
|
-
# It's used for cases when some 3rd parties are relies on the validators
|
|
5
|
-
# For example, +required+ option from simple_form checks the validator
|
|
6
|
-
def self.kind
|
|
7
|
-
:presence
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
attr_reader :foreign_key, :association
|
|
11
|
-
|
|
12
|
-
def initialize(options)
|
|
13
|
-
super(options)
|
|
14
|
-
|
|
15
|
-
@association = attributes.first
|
|
16
|
-
@foreign_key = options[:foreign_key]
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
# The else statement required only for optional: false
|
|
20
|
-
def validate(record)
|
|
21
|
-
if record._database_validations_fallback
|
|
22
|
-
super
|
|
23
|
-
else
|
|
24
|
-
return unless record.public_send(foreign_key).blank? && record.public_send(association).blank?
|
|
25
|
-
|
|
26
|
-
record.errors.add(association, :blank, message: BelongsToOptions::VALIDATOR_MESSAGE)
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
module DatabaseValidations
|
|
2
|
-
module Helpers
|
|
3
|
-
module_function
|
|
4
|
-
|
|
5
|
-
def cache_valid_method!(klass)
|
|
6
|
-
return if klass.method_defined?(:valid_without_database_validations?)
|
|
7
|
-
|
|
8
|
-
klass.__send__(:alias_method, :valid_without_database_validations?, :valid?)
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
def handle_error!(instance, error)
|
|
12
|
-
case error
|
|
13
|
-
when ActiveRecord::RecordNotUnique
|
|
14
|
-
handle_unique_error!(instance, error)
|
|
15
|
-
when ActiveRecord::InvalidForeignKey
|
|
16
|
-
handle_foreign_key_error!(instance, error)
|
|
17
|
-
else false
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def handle_unique_error!(instance, error)
|
|
22
|
-
adapter = Adapters.factory(instance.class)
|
|
23
|
-
|
|
24
|
-
keys = [
|
|
25
|
-
generate_key_for_uniqueness_index(adapter.unique_index_name(error.message)),
|
|
26
|
-
generate_key_for_uniqueness(adapter.unique_error_columns(error.message))
|
|
27
|
-
]
|
|
28
|
-
|
|
29
|
-
each_options_storage(instance.class) do |storage|
|
|
30
|
-
keys.each { |key| return storage[key].handle_unique_error(instance) if storage[key] }
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
false
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def handle_foreign_key_error!(instance, error)
|
|
37
|
-
adapter = Adapters.factory(instance.class)
|
|
38
|
-
column_key = generate_key_for_belongs_to(adapter.foreign_key_error_column(error.message))
|
|
39
|
-
|
|
40
|
-
each_options_storage(instance.class) do |storage|
|
|
41
|
-
return storage[column_key].handle_foreign_key_error(instance) if storage[column_key]
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
false
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def each_options_storage(klass)
|
|
48
|
-
while klass.respond_to?(:validates_db_uniqueness_of)
|
|
49
|
-
storage = storage_of(klass)
|
|
50
|
-
yield(storage) if storage
|
|
51
|
-
klass = klass.superclass
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def storage_of(klass)
|
|
56
|
-
klass.instance_variable_get(:'@database_validations_storage')
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def unify_columns(*args)
|
|
60
|
-
args.flatten.compact.map(&:to_s).sort
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def generate_key_for_uniqueness_index(index_name)
|
|
64
|
-
generate_key(:uniqueness_index, index_name)
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def generate_key_for_uniqueness(*columns)
|
|
68
|
-
generate_key(:uniqueness, columns)
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def generate_key_for_belongs_to(column)
|
|
72
|
-
generate_key(:belongs_to, column)
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def generate_key(type, *args)
|
|
76
|
-
[type, *unify_columns(args)].join('__')
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
end
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
module DatabaseValidations
|
|
2
|
-
class OptionsStorage
|
|
3
|
-
def initialize(klass)
|
|
4
|
-
@adapter = Adapters.factory(klass).new(klass)
|
|
5
|
-
@storage = {}
|
|
6
|
-
end
|
|
7
|
-
|
|
8
|
-
def push_uniqueness(field, options)
|
|
9
|
-
uniqueness_options = UniquenessOptions.new(field, options, adapter)
|
|
10
|
-
storage[uniqueness_options.index_key] = uniqueness_options
|
|
11
|
-
storage[uniqueness_options.column_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 options
|
|
24
|
-
storage.values
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
private
|
|
28
|
-
|
|
29
|
-
attr_reader :storage, :adapter
|
|
30
|
-
end
|
|
31
|
-
end
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
module DatabaseValidations
|
|
2
|
-
class DBUniquenessValidator < ActiveRecord::Validations::UniquenessValidator
|
|
3
|
-
# This is a hack to simulate presence validator
|
|
4
|
-
# It's used for cases when some 3rd parties are relies on the validators
|
|
5
|
-
# For example, +required+ option from simple_form checks the validator
|
|
6
|
-
def self.kind
|
|
7
|
-
:uniqueness
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
def validate(record)
|
|
11
|
-
super if record._database_validations_fallback
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
end
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
module DatabaseValidations
|
|
2
|
-
module ClassMethods
|
|
3
|
-
def validates_db_uniqueness_of(*attributes)
|
|
4
|
-
Helpers.cache_valid_method!(self)
|
|
5
|
-
|
|
6
|
-
@database_validations_storage ||= DatabaseValidations::OptionsStorage.new(self)
|
|
7
|
-
|
|
8
|
-
options = attributes.extract_options!
|
|
9
|
-
|
|
10
|
-
attributes.each do |attribute|
|
|
11
|
-
@database_validations_storage.push_uniqueness(attribute, options.merge(attributes: attribute))
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
validates_with DatabaseValidations::DBUniquenessValidator,
|
|
15
|
-
DatabaseValidations::UniquenessOptions.validator_options(attributes, options)
|
|
16
|
-
|
|
17
|
-
include(DatabaseValidations::Rescuer)
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
end
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
module DatabaseValidations
|
|
2
|
-
class UniquenessOptions
|
|
3
|
-
CUSTOM_OPTIONS = %i[where index_name].freeze
|
|
4
|
-
DEFAULT_OPTIONS = { allow_nil: true, case_sensitive: true, allow_blank: false }.freeze
|
|
5
|
-
|
|
6
|
-
def self.validator_options(attributes, options)
|
|
7
|
-
DEFAULT_OPTIONS
|
|
8
|
-
.merge(attributes: attributes)
|
|
9
|
-
.merge(options)
|
|
10
|
-
.except(*CUSTOM_OPTIONS)
|
|
11
|
-
.tap { |opts| opts[:conditions] = -> { where(options[:where]) } if options[:where] }
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
attr_reader :field, :calculated_index_name
|
|
15
|
-
|
|
16
|
-
def initialize(field, options, adapter)
|
|
17
|
-
@field = field
|
|
18
|
-
@options = options
|
|
19
|
-
@adapter = adapter
|
|
20
|
-
|
|
21
|
-
raise_if_unsupported_options!
|
|
22
|
-
|
|
23
|
-
return if ENV['SKIP_DB_UNIQUENESS_VALIDATOR_INDEX_CHECK']
|
|
24
|
-
|
|
25
|
-
index = responsible_index
|
|
26
|
-
raise_if_index_missed!(index)
|
|
27
|
-
|
|
28
|
-
@calculated_index_name = index.name
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
def handle_unique_error(instance)
|
|
32
|
-
error_options = options.except(:case_sensitive, :scope, :conditions, :attributes, *CUSTOM_OPTIONS)
|
|
33
|
-
error_options[:value] = instance.public_send(options[:attributes])
|
|
34
|
-
|
|
35
|
-
instance.errors.add(options[:attributes], :taken, error_options)
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
# @return [String]
|
|
39
|
-
def index_key
|
|
40
|
-
@index_key ||= Helpers.generate_key_for_uniqueness_index(index_name || calculated_index_name)
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
# @return [String]
|
|
44
|
-
def column_key
|
|
45
|
-
@column_key ||= Helpers.generate_key_for_uniqueness(columns)
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
# @return [Array<String>]
|
|
49
|
-
def columns
|
|
50
|
-
@columns ||= Helpers.unify_columns(field, scope)
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
# @return [String|nil]
|
|
54
|
-
def where_clause
|
|
55
|
-
@where_clause ||= options[:where]
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
# @return [String|nil]
|
|
59
|
-
def message
|
|
60
|
-
@message ||= options[:message]
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
# @return [Array<String|Symbol>]
|
|
64
|
-
def scope
|
|
65
|
-
@scope ||= Array.wrap(options[:scope])
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
# @return [String|Symbol|nil]
|
|
69
|
-
def index_name
|
|
70
|
-
@index_name ||= options[:index_name]
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
# @return [Boolean|nil]
|
|
74
|
-
def case_sensitive
|
|
75
|
-
@case_sensitive ||= options[:case_sensitive]
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
private
|
|
79
|
-
|
|
80
|
-
attr_reader :adapter, :options
|
|
81
|
-
|
|
82
|
-
def raise_if_unsupported_options!
|
|
83
|
-
options.except(:attributes).each_key do |option|
|
|
84
|
-
unless adapter.support_option?(option)
|
|
85
|
-
raise Errors::OptionIsNotSupported.new(option, adapter.adapter_name, adapter.supported_options)
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
def responsible_index
|
|
91
|
-
index_name ? adapter.find_index_by_name(index_name.to_s) : adapter.find_index(columns, where_clause)
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def index_columns_size(columns)
|
|
95
|
-
columns.is_a?(Array) ? columns.size : (columns.count(',') + 1)
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def check_index_options?(index)
|
|
99
|
-
(columns.size == index_columns_size(index.columns)) && (where_clause.nil? == index.where.nil?)
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
def raise_if_index_missed!(index)
|
|
103
|
-
return if index && check_index_options?(index)
|
|
104
|
-
|
|
105
|
-
raise Errors::IndexNotFound.new(columns, where_clause, index_name, adapter.indexes, adapter.table_name)
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
end
|