Skip to content

Commit

Permalink
Extract MySQL-specific code into Adapters module
Browse files Browse the repository at this point in the history
  • Loading branch information
shioyama committed Jan 30, 2025
1 parent f115529 commit 844c4d0
Show file tree
Hide file tree
Showing 7 changed files with 122 additions and 82 deletions.
3 changes: 2 additions & 1 deletion lib/active_model/validations/bytesize.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ def check_validity!
end

def validate_each(record, attribute, value)
string = ActiveRecord::DatabaseValidations::MySQL.value_for_column(value, options[:encoding])
string = value.to_s
string = value.encode(Encoding::UTF_8) if value.present? && value.encoding != options[:encoding]

if string.bytesize > options[:maximum]
errors_options = options.except(:too_many_bytes, :maximum)
Expand Down
2 changes: 1 addition & 1 deletion lib/active_record/database_validations.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
require 'active_record'
require 'active_support/i18n'
require 'active_record/database_validations/version'
require 'active_record/database_validations/adapters'

require 'active_record/database_validations/mysql'
require 'active_record/validations/database_constraints'
require 'active_record/validations/string_truncator'
require 'active_record/validations/typed_column'
Expand Down
32 changes: 32 additions & 0 deletions lib/active_record/database_validations/adapters.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
module ActiveRecord
module DatabaseValidations
module Adapters
class MissingAdapterError < StandardError; end

@registry = {}

def self.for(column)
name = column.class.name.dup
name.delete_prefix!("ActiveRecord::ConnectionAdapters::")
name.delete_suffix!("::Column")

registry.fetch(name).call
rescue LoadError, KeyError
raise MissingAdapterError, "no adapter found for #{name}"
end

def self.register(name, &loader)
@registry[name] = loader
end

class << self
attr_reader :registry
end
end

Adapters.register("MySQL") do
require "active_record/database_validations/adapters/mysql"
ActiveRecord::DatabaseValidations::Adapters::MySQL
end
end
end
78 changes: 78 additions & 0 deletions lib/active_record/database_validations/adapters/mysql.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
module ActiveRecord
module DatabaseValidations
module Adapters
class MySQL
TYPE_LIMITS = {
char: { type: :characters },
varchar: { type: :characters },
varbinary: { type: :bytes },

tinytext: { type: :bytes, maximum: 2 ** 8 - 1 },
text: { type: :bytes, maximum: 2 ** 16 - 1 },
mediumtext: { type: :bytes, maximum: 2 ** 24 - 1 },
longtext: { type: :bytes, maximum: 2 ** 32 - 1 },

tinyblob: { type: :bytes, maximum: 2 ** 8 - 1 },
blob: { type: :bytes, maximum: 2 ** 16 - 1 },
mediumblob: { type: :bytes, maximum: 2 ** 24 - 1 },
longblob: { type: :bytes, maximum: 2 ** 32 - 1 },
}

def self.column_size_limit(column)
column_type = column.sql_type.sub(/\(.*\z/, '').gsub(/\s/, '_').to_sym
type_limit = TYPE_LIMITS.fetch(column_type, {})

[
column.limit || type_limit[:maximum],
type_limit[:type],
determine_encoding(column),
]
end

def self.column_range(column)
args = {}
unsigned = column.sql_type =~ / unsigned\z/
case column.type
when :decimal
args[:less_than] = maximum = 10 ** (column.precision - column.scale)
if unsigned
args[:greater_than_or_equal_to] = 0
else
args[:greater_than] = 0 - maximum
end

when :integer
args[:only_integer] = true
args[:less_than] = unsigned ? 1 << (column.limit * 8) : 1 << (column.limit * 8 - 1)
args[:greater_than_or_equal_to] = unsigned ? 0 : 0 - args[:less_than]
end

args
end

def self.determine_encoding(column)
column = ActiveRecord::Validations::TypedColumn.new(column)
return nil unless column.text?
case column.collation
when /\Autf8/; Encoding::UTF_8
else raise NotImplementedError, "Don't know how to determine the Ruby encoding for MySQL's #{column.collation} collation."
end
end

def self.requires_transcoding?(value, column_encoding = nil)
column_encoding.present? && column_encoding != value.encoding
end

def self.value_for_column(value, column_encoding = nil)
value = value.to_s unless value.is_a?(String)

if requires_transcoding?(value, column_encoding)
return value.encode(Encoding::UTF_8)
end

value
end
end
end
end
end
76 changes: 0 additions & 76 deletions lib/active_record/database_validations/mysql.rb

This file was deleted.

8 changes: 6 additions & 2 deletions lib/active_record/validations/database_constraints.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def size_validator(klass, column)
return unless constraints.include?(:size)
return unless column.text? || column.binary?

maximum, type, encoding = ActiveRecord::DatabaseValidations::MySQL.column_size_limit(column)
maximum, type, encoding = adapter_for(column).column_size_limit(column)
validator_class = SIZE_VALIDATORS_FOR_TYPE[type]

if validator_class && maximum
Expand All @@ -50,7 +50,7 @@ def range_validator(klass, column)
return unless column.number?

args = { attributes: [column.name.to_sym], class: klass, allow_nil: true }
args.merge!(ActiveRecord::DatabaseValidations::MySQL.column_range(column))
args.merge!(adapter_for(column).column_range(column))
ActiveModel::Validations::NumericalityValidator.new(args)
end

Expand All @@ -77,6 +77,10 @@ def validate_each(record, attribute, _value)
validator.validate(record)
end
end

def adapter_for(column)
DatabaseValidations::Adapters.for(column.__getobj__)
end
end
end
end
Expand Down
5 changes: 3 additions & 2 deletions lib/active_record/validations/string_truncator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ def truncate_value_to_field_limit(field, value)
return if value.nil?

column = self.class.columns_hash[field.to_s]
maximum, type, encoding = ActiveRecord::DatabaseValidations::MySQL.column_size_limit(column)
value = ActiveRecord::DatabaseValidations::MySQL.value_for_column(value, encoding)
adapter = DatabaseValidations::Adapters.for(column)
maximum, type, encoding = adapter.column_size_limit(column)
value = adapter.value_for_column(value, encoding)

case type
when :characters
Expand Down

0 comments on commit 844c4d0

Please sign in to comment.