database_validations 0.8.10 → 0.9.4

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.
Files changed (30) hide show
  1. checksums.yaml +4 -4
  2. data/lib/database_validations.rb +12 -9
  3. data/lib/database_validations/lib/adapters/base_adapter.rb +13 -22
  4. data/lib/database_validations/lib/adapters/mysql_adapter.rb +1 -2
  5. data/lib/database_validations/lib/adapters/postgresql_adapter.rb +0 -1
  6. data/lib/database_validations/lib/adapters/sqlite_adapter.rb +0 -1
  7. data/lib/database_validations/lib/attribute_validator.rb +3 -0
  8. data/lib/database_validations/lib/checkers/db_presence_validator.rb +36 -0
  9. data/lib/database_validations/lib/checkers/db_uniqueness_validator.rb +47 -0
  10. data/lib/database_validations/lib/errors.rb +0 -11
  11. data/lib/database_validations/lib/injector.rb +13 -0
  12. data/lib/database_validations/lib/key_generator.rb +32 -0
  13. data/lib/database_validations/lib/presence_key_extractor.rb +18 -0
  14. data/lib/database_validations/lib/rescuer.rb +28 -17
  15. data/lib/database_validations/lib/storage.rb +28 -0
  16. data/lib/database_validations/lib/uniqueness_key_extractor.rb +38 -0
  17. data/lib/database_validations/lib/validations.rb +41 -0
  18. data/lib/database_validations/lib/validators/db_presence_validator.rb +67 -0
  19. data/lib/database_validations/lib/validators/db_uniqueness_validator.rb +65 -0
  20. data/lib/database_validations/rspec/uniqueness_validator_matcher.rb +15 -8
  21. data/lib/database_validations/version.rb +1 -1
  22. metadata +37 -21
  23. data/lib/database_validations/lib/db_belongs_to/belongs_to_handlers.rb +0 -30
  24. data/lib/database_validations/lib/db_belongs_to/belongs_to_options.rb +0 -46
  25. data/lib/database_validations/lib/db_belongs_to/db_presence_validator.rb +0 -30
  26. data/lib/database_validations/lib/helpers.rb +0 -79
  27. data/lib/database_validations/lib/options_storage.rb +0 -31
  28. data/lib/database_validations/lib/validates_db_uniqueness_of/db_uniqueness_validator.rb +0 -14
  29. data/lib/database_validations/lib/validates_db_uniqueness_of/uniqueness_handlers.rb +0 -20
  30. 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: c934b2c71117e26cc8493d7eafb35a681cb54f77bb6a1ad7d9ba0ec309664817
4
- data.tar.gz: 7b7e34001d44eac5b818d756288ecb5e5d7f33147d5f8bbc07a3e54b0de6287c
3
+ metadata.gz: 5cea34b87885accbde526d9a36bcfcda30b0ae23dd474ee9d3ccbd19b729cdb5
4
+ data.tar.gz: bf86e7012afed03710c09ab205388d08a6c0c3b34a8cce77423fca40916ae03e
5
5
  SHA512:
6
- metadata.gz: 544cee2e2edf40aec5a25bfdc789df6324d07120997a1a68b366eac75152eeb095ca9cfb3758c32842e189da2ad5306e4530faaa24fa2ecbf44c93c44995e2c9
7
- data.tar.gz: 3e006dfe1110c3758a3a971a2db45660a73427e6b9007d140f1e338d8dd02bd099c5a9cd29f78b8018c1f541c8c67f4bd064a845bbbf5bb6f1a18ff21d6d679c
6
+ metadata.gz: 1ed0995301de6de0e5169d0e5b3e333ce9b0417014e423094aa5293055dec0ee8657f1a8661d466f8ccdce64020e33378c4c4f4623da5fa5b6755408e0253ff4
7
+ data.tar.gz: 3d08f422bff52c0e3942dbdb60ab867c891bd1802f12f90fd97305a66bc5bdf01158aa5ba94c418fab6e8ed88db41cc05cacaee47a7ddad95d77dffdfd6c799e
@@ -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/validates_db_uniqueness_of/db_uniqueness_validator'
8
- require 'database_validations/lib/validates_db_uniqueness_of/uniqueness_handlers'
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/db_belongs_to/db_presence_validator'
12
- require 'database_validations/lib/db_belongs_to/belongs_to_handlers'
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/rescuer'
16
- require 'database_validations/lib/options_storage'
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/helpers'
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
@@ -9,18 +9,25 @@ module DatabaseValidations
9
9
  end
10
10
 
11
11
  # @param [String] index_name
12
- def find_index_by_name(index_name)
13
- indexes.find { |index| index.name == index_name }
12
+ def find_unique_index_by_name(index_name)
13
+ unique_indexes.find { |index| index.name == index_name }
14
14
  end
15
15
 
16
16
  # @param [Array<String>] columns
17
17
  # @param [String] where
18
- def find_index(columns, where)
19
- indexes.find { |index| Array.wrap(index.columns).map(&:to_s).sort == columns && index.where == where }
18
+ def find_unique_index(columns, where)
19
+ unique_indexes.find { |index| Array.wrap(index.columns).map(&:to_s).sort == columns && index.where == where }
20
20
  end
21
21
 
22
- def indexes
23
- model.connection.indexes(model.table_name).select(&:unique)
22
+ def unique_indexes
23
+ connection = model.connection
24
+
25
+ if connection.schema_cache.respond_to?(:indexes)
26
+ # Rails 6 only
27
+ connection.schema_cache.indexes(model.table_name).select(&:unique)
28
+ else
29
+ connection.indexes(model.table_name).select(&:unique)
30
+ end
24
31
  end
25
32
 
26
33
  def foreign_keys
@@ -31,22 +38,6 @@ module DatabaseValidations
31
38
  foreign_keys.find { |foreign_key| foreign_key.column.to_s == column.to_s }
32
39
  end
33
40
 
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
41
  # @return [String]
51
42
  def table_name
52
43
  model.table_name
@@ -1,12 +1,11 @@
1
1
  module DatabaseValidations
2
2
  module Adapters
3
3
  class MysqlAdapter < BaseAdapter
4
- SUPPORTED_OPTIONS = %i[scope message if unless index_name].freeze
5
4
  ADAPTER = :mysql2
6
5
 
7
6
  class << self
8
7
  def unique_index_name(error_message)
9
- error_message[/key '([^']+)'/, 1]
8
+ error_message[/key '([^']+)'/, 1]&.split('.')&.last
10
9
  end
11
10
 
12
11
  def unique_error_columns(_error_message); end
@@ -1,7 +1,6 @@
1
1
  module DatabaseValidations
2
2
  module Adapters
3
3
  class PostgresqlAdapter < BaseAdapter
4
- SUPPORTED_OPTIONS = %i[scope message where if unless index_name case_sensitive].freeze
5
4
  ADAPTER = :postgresql
6
5
 
7
6
  class << self
@@ -1,7 +1,6 @@
1
1
  module DatabaseValidations
2
2
  module Adapters
3
3
  class SqliteAdapter < BaseAdapter
4
- SUPPORTED_OPTIONS = %i[scope message if unless].freeze
5
4
  ADAPTER = :sqlite3
6
5
 
7
6
  class << self
@@ -0,0 +1,3 @@
1
+ module DatabaseValidations
2
+ AttributeValidator = Struct.new(:attribute, :validator)
3
+ end
@@ -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_unique_index_by_name(validator.index_name.to_s) : adapter.find_unique_index(columns, validator.where) # rubocop:disable Metrics/LineLength
42
+ raise Errors::IndexNotFound.new(columns, validator.where, validator.index_name, adapter.unique_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,42 @@
1
1
  module DatabaseValidations
2
2
  module Rescuer
3
- extend ActiveSupport::Concern
3
+ module_function
4
4
 
5
- included do
6
- alias_method :validate, :valid?
5
+ def handled?(instance, error)
6
+ Storage.prepare(instance.class) unless Storage.prepared?(instance.class)
7
+
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
7
15
  end
8
16
 
9
- attr_accessor :_database_validations_fallback
17
+ def process(instance, error, key_types)
18
+ adapter = Adapters.factory(instance.class)
10
19
 
11
- def valid?(context = nil)
12
- self._database_validations_fallback = true
13
- super(context)
14
- end
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]
26
+
27
+ next unless attribute_validator
15
28
 
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 => error
20
- raise error unless Helpers.handle_error!(self, error)
29
+ return process_validator(instance, attribute_validator)
30
+ end
21
31
 
22
- raise ActiveRecord::RecordInvalid, self
32
+ false
23
33
  end
24
34
 
25
- private
35
+ def process_validator(instance, attribute_validator)
36
+ return false unless attribute_validator.validator.perform_db_validation?
26
37
 
27
- def perform_validations(options = {})
28
- options[:validate] == false || valid_without_database_validations?(options[:context])
38
+ attribute_validator.validator.apply_error(instance, attribute_validator.attribute)
39
+ true
29
40
  end
30
41
  end
31
42
  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_unique_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,41 @@
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
+ options = args.extract_options!
18
+
19
+ if options[:validate] == false
20
+ super
21
+ else
22
+ rescue_from_database_exceptions { super }
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def rescue_from_database_exceptions(&block)
29
+ self._database_validations_fallback = false
30
+ self.class.connection.transaction(requires_new: true, &block)
31
+ rescue ActiveRecord::InvalidForeignKey, ActiveRecord::RecordNotUnique => e
32
+ raise e unless Rescuer.handled?(self, e)
33
+
34
+ raise ActiveRecord::RecordInvalid, self
35
+ end
36
+
37
+ def perform_validations(options = {})
38
+ options[:validate] == false || valid_without_database_validations?(options[:context])
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,67 @@
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
+ def perform_db_validation?
25
+ true
26
+ end
27
+
28
+ # TODO: add support of optional db_belongs_to
29
+ def validate(record)
30
+ if record._database_validations_fallback
31
+ super
32
+ else
33
+ attributes.each do |attribute|
34
+ reflection = record.class._reflect_on_association(attribute)
35
+
36
+ next if reflection && record.public_send(reflection.foreign_key).present?
37
+
38
+ validate_each(record, attribute, record.public_send(attribute))
39
+ end
40
+ end
41
+ end
42
+
43
+ def apply_error(instance, attribute)
44
+ # Helps to avoid querying the database when attribute is association
45
+ instance.send("#{attribute}=", nil)
46
+ instance.errors.add(attribute, :blank, message: REFLECTION_MESSAGE)
47
+ end
48
+ end
49
+
50
+ module ClassMethods
51
+ def validates_db_presence_of(*attr_names)
52
+ validates_with(DatabaseValidations::DbPresenceValidator, _merge_attributes(attr_names))
53
+ end
54
+
55
+ def db_belongs_to(name, scope = nil, **options)
56
+ if ActiveRecord::VERSION::MAJOR < 5
57
+ options[:required] = false
58
+ else
59
+ options[:optional] = true
60
+ end
61
+
62
+ belongs_to(name, scope, **options)
63
+
64
+ validates_with DatabaseValidations::DbPresenceValidator, _merge_attributes([name, message: DatabaseValidations::DbPresenceValidator::REFLECTION_MESSAGE]) # rubocop:disable Metrics/LineLength
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,65 @@
1
+ module DatabaseValidations
2
+ class DbUniquenessValidator < ActiveRecord::Validations::UniquenessValidator
3
+ DEFAULT_MODE = :optimized
4
+
5
+ attr_reader :index_name, :where, :klass
6
+
7
+ # Used to make 3rd party libraries work correctly
8
+ #
9
+ # @return [Symbol]
10
+ def self.kind
11
+ :uniqueness
12
+ end
13
+
14
+ # @param [Hash] options
15
+ def initialize(options)
16
+ options[:allow_nil] = true
17
+ options[:allow_blank] = false
18
+
19
+ if options.key?(:where)
20
+ condition = options[:where]
21
+ options[:conditions] = -> { where(condition) }
22
+ end
23
+
24
+ handle_custom_options(options)
25
+
26
+ super
27
+
28
+ Injector.inject(klass)
29
+ Checkers::DbUniquenessValidator.validate!(self)
30
+ end
31
+
32
+ def perform_db_validation?
33
+ @mode != :standard
34
+ end
35
+
36
+ def validate(record)
37
+ super if perform_query? || record._database_validations_fallback
38
+ end
39
+
40
+ def apply_error(instance, attribute)
41
+ error_options = options.except(:case_sensitive, :scope, :conditions)
42
+ error_options[:value] = instance.public_send(attribute)
43
+
44
+ instance.errors.add(attribute, :taken, error_options)
45
+ end
46
+
47
+ private
48
+
49
+ def handle_custom_options(options)
50
+ @index_name = options.delete(:index_name) if options.key?(:index_name)
51
+ @where = options.delete(:where) if options.key?(:where)
52
+ @mode = (options.delete(:mode).presence || DEFAULT_MODE).to_sym
53
+ end
54
+
55
+ def perform_query?
56
+ @mode != :optimized
57
+ end
58
+ end
59
+
60
+ module ClassMethods
61
+ def validates_db_uniqueness_of(*attr_names)
62
+ validates_with(DatabaseValidations::DbUniquenessValidator, _merge_attributes(attr_names))
63
+ end
64
+ end
65
+ end
@@ -41,31 +41,37 @@ RSpec::Matchers.define :validate_db_uniqueness_of do |field| # rubocop:disable M
41
41
  @case_sensitive = false
42
42
  end
43
43
 
44
+ chain(:case_sensitive) do
45
+ @case_sensitive = true
46
+ end
47
+
44
48
  match do |object|
45
49
  @validators = []
46
50
 
47
51
  model = object.is_a?(Class) ? object : object.class
48
52
 
49
- DatabaseValidations::Helpers.each_options_storage(model) do |storage|
50
- storage.options.grep(DatabaseValidations::UniquenessOptions).each do |validator|
53
+ model.validators.grep(DatabaseValidations::DbUniquenessValidator).each do |validator|
54
+ validator.attributes.each do |attribute|
51
55
  @validators << {
52
- field: validator.field,
53
- scope: validator.scope,
54
- where: validator.where_clause,
55
- message: validator.message,
56
+ field: attribute,
57
+ scope: Array.wrap(validator.options[:scope]),
58
+ where: validator.where,
59
+ message: validator.options[:message],
56
60
  index_name: validator.index_name,
57
- case_sensitive: validator.case_sensitive
61
+ case_sensitive: validator.options[:case_sensitive]
58
62
  }
59
63
  end
60
64
  end
61
65
 
66
+ case_sensitive_default = ActiveRecord::VERSION::MAJOR >= 6 ? nil : true
67
+
62
68
  @validators.include?(
63
69
  field: field,
64
70
  scope: Array.wrap(@scope),
65
71
  where: @where,
66
72
  message: @message,
67
73
  index_name: @index_name,
68
- case_sensitive: @case_sensitive
74
+ case_sensitive: @case_sensitive.nil? ? case_sensitive_default : @case_sensitive
69
75
  )
70
76
  end
71
77
 
@@ -77,6 +83,7 @@ RSpec::Matchers.define :validate_db_uniqueness_of do |field| # rubocop:disable M
77
83
  desc += "where: '#{@where}'; " if @where
78
84
  desc += "index_name: '#{@index_name}'; " if @index_name
79
85
  desc += 'be case insensitive.' unless @case_sensitive
86
+ desc += 'be case sensitive.' if @case_sensitive
80
87
  desc
81
88
  end
82
89
 
@@ -1,3 +1,3 @@
1
1
  module DatabaseValidations
2
- VERSION = '0.8.10'.freeze
2
+ VERSION = '0.9.4'.freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: database_validations
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.10
4
+ version: 0.9.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Evgeniy Demin
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-02-21 00:00:00.000000000 Z
11
+ date: 2020-09-30 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: '1.16'
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: '1.16'
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: '10.0'
103
+ version: '13.0'
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: '10.0'
110
+ version: '13.0'
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.0
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.0
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/db_belongs_to/belongs_to_handlers.rb
173
- - lib/database_validations/lib/db_belongs_to/belongs_to_options.rb
174
- - lib/database_validations/lib/db_belongs_to/db_presence_validator.rb
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/helpers.rb
177
- - lib/database_validations/lib/options_storage.rb
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/validates_db_uniqueness_of/db_uniqueness_validator.rb
180
- - lib/database_validations/lib/validates_db_uniqueness_of/uniqueness_handlers.rb
181
- - lib/database_validations/lib/validates_db_uniqueness_of/uniqueness_options.rb
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
@@ -191,7 +208,7 @@ homepage: https://github.com/toptal/database_validations
191
208
  licenses:
192
209
  - MIT
193
210
  metadata: {}
194
- post_install_message:
211
+ post_install_message:
195
212
  rdoc_options: []
196
213
  require_paths:
197
214
  - lib
@@ -206,9 +223,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
206
223
  - !ruby/object:Gem::Version
207
224
  version: '0'
208
225
  requirements: []
209
- rubyforge_project:
210
- rubygems_version: 2.7.8
211
- signing_key:
226
+ rubygems_version: 3.0.8
227
+ signing_key:
212
228
  specification_version: 4
213
229
  summary: Provide compatibility between database constraints and ActiveRecord validations
214
230
  with better performance and consistency.
@@ -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