mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Consistently apply adapter behavior when serializing arrays
In f1a0fa9
we moved backend specific timestamp behavior out of the type
and into the adapter. This was in line with our general attempt to
reduce the number of adapter specific type subclasses. However, on PG,
the array type performs all serialization, including database encoding
in its serialize method.
This means that we have converted the value into a string before
reaching the database, so no adapter specific logic can be applied (and
this also means that timestamp arrays were using the default `.to_s`
method on the given object, which likely meant timestamps were being
ignored in certain cases as well)
Ultimately I want to do a more in depth refactoring which separates
database serializer objects from the active model type objects, to give
us a less awkward API for introducing the attributes API onto Active
Model.
However, in the short term, we follow the solution we've applied
elsewhere for this. Move behavior off of the type and into the adapter,
and use a data object to allow the type to communicate information up
the stack.
Fixes #27514.
This commit is contained in:
parent
0d8069d365
commit
0f1d0b1b52
5 changed files with 62 additions and 17 deletions
|
@ -1,3 +1,9 @@
|
|||
* Respect precision option for arrays of timestamps.
|
||||
|
||||
Fixes #27514.
|
||||
|
||||
*Sean Griffin*
|
||||
|
||||
* Optimize slow model instantiation when using STI and `store_full_sti_class = false` option.
|
||||
|
||||
*Konstantin Lazarev*
|
||||
|
|
|
@ -5,6 +5,8 @@ module ActiveRecord
|
|||
class Array < Type::Value # :nodoc:
|
||||
include Type::Helpers::Mutable
|
||||
|
||||
Data = Struct.new(:encoder, :values) # :nodoc:
|
||||
|
||||
attr_reader :subtype, :delimiter
|
||||
delegate :type, :user_input_in_time_zone, :limit, :precision, :scale, to: :subtype
|
||||
|
||||
|
@ -17,8 +19,11 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
def deserialize(value)
|
||||
if value.is_a?(::String)
|
||||
case value
|
||||
when ::String
|
||||
type_cast_array(@pg_decoder.decode(value), :deserialize)
|
||||
when Data
|
||||
deserialize(value.values)
|
||||
else
|
||||
super
|
||||
end
|
||||
|
@ -33,11 +38,8 @@ module ActiveRecord
|
|||
|
||||
def serialize(value)
|
||||
if value.is_a?(::Array)
|
||||
result = @pg_encoder.encode(type_cast_array(value, :serialize))
|
||||
if encoding = determine_encoding_of_strings(value)
|
||||
result.force_encoding(encoding)
|
||||
end
|
||||
result
|
||||
casted_values = type_cast_array(value, :serialize)
|
||||
Data.new(@pg_encoder, casted_values)
|
||||
else
|
||||
super
|
||||
end
|
||||
|
@ -58,6 +60,10 @@ module ActiveRecord
|
|||
value.map(&block)
|
||||
end
|
||||
|
||||
def changed_in_place?(raw_old_value, new_value)
|
||||
deserialize(raw_old_value) != new_value
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def type_cast_array(value, method)
|
||||
|
@ -67,13 +73,6 @@ module ActiveRecord
|
|||
@subtype.public_send(method, value)
|
||||
end
|
||||
end
|
||||
|
||||
def determine_encoding_of_strings(value)
|
||||
case value
|
||||
when ::Array then determine_encoding_of_strings(value.first)
|
||||
when ::String then value.encoding
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -92,6 +92,8 @@ module ActiveRecord
|
|||
else
|
||||
super
|
||||
end
|
||||
when OID::Array::Data
|
||||
_quote(encode_array(value))
|
||||
else
|
||||
super
|
||||
end
|
||||
|
@ -106,10 +108,37 @@ module ActiveRecord
|
|||
{ value: value.to_s, format: 1 }
|
||||
when OID::Xml::Data, OID::Bit::Data
|
||||
value.to_s
|
||||
when OID::Array::Data
|
||||
encode_array(value)
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def encode_array(array_data)
|
||||
encoder = array_data.encoder
|
||||
values = type_cast_array(array_data.values)
|
||||
|
||||
result = encoder.encode(values)
|
||||
if encoding = determine_encoding_of_strings_in_array(values)
|
||||
result.force_encoding(encoding)
|
||||
end
|
||||
result
|
||||
end
|
||||
|
||||
def determine_encoding_of_strings_in_array(value)
|
||||
case value
|
||||
when ::Array then determine_encoding_of_strings_in_array(value.first)
|
||||
when ::String then value.encoding
|
||||
end
|
||||
end
|
||||
|
||||
def type_cast_array(values)
|
||||
case values
|
||||
when ::Array then values.map { |item| type_cast_array(item) }
|
||||
else _type_cast(values)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -21,6 +21,7 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase
|
|||
t.datetime :datetimes, array: true
|
||||
t.hstore :hstores, array: true
|
||||
t.decimal :decimals, array: true, default: [], precision: 10, scale: 2
|
||||
t.timestamp :timestamps, array: true, default: [], precision: 6
|
||||
end
|
||||
end
|
||||
PgArray.reset_column_information
|
||||
|
@ -213,7 +214,7 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase
|
|||
x = PgArray.create!(tags: tags)
|
||||
x.reload
|
||||
|
||||
assert_equal x.tags_before_type_cast, PgArray.type_for_attribute("tags").serialize(tags)
|
||||
refute x.changed?
|
||||
end
|
||||
|
||||
def test_quoting_non_standard_delimiters
|
||||
|
@ -221,9 +222,10 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase
|
|||
oid = ActiveRecord::ConnectionAdapters::PostgreSQL::OID
|
||||
comma_delim = oid::Array.new(ActiveRecord::Type::String.new, ",")
|
||||
semicolon_delim = oid::Array.new(ActiveRecord::Type::String.new, ";")
|
||||
conn = PgArray.connection
|
||||
|
||||
assert_equal %({"hello,",world;}), comma_delim.serialize(strings)
|
||||
assert_equal %({hello,;"world;"}), semicolon_delim.serialize(strings)
|
||||
assert_equal %({"hello,",world;}), conn.type_cast(comma_delim.serialize(strings))
|
||||
assert_equal %({hello,;"world;"}), conn.type_cast(semicolon_delim.serialize(strings))
|
||||
end
|
||||
|
||||
def test_mutate_array
|
||||
|
@ -319,6 +321,15 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase
|
|||
assert_equal [arrays_of_utf8_strings], @type.deserialize(@type.serialize([arrays_of_utf8_strings]))
|
||||
end
|
||||
|
||||
def test_precision_is_respected_on_timestamp_columns
|
||||
time = Time.now.change(usec: 123)
|
||||
record = PgArray.create!(timestamps: [time])
|
||||
|
||||
assert_equal 123, record.timestamps.first.usec
|
||||
record.reload
|
||||
assert_equal 123, record.timestamps.first.usec
|
||||
end
|
||||
|
||||
private
|
||||
def assert_cycle(field, array)
|
||||
# test creation
|
||||
|
|
|
@ -19,7 +19,7 @@ class PostgresqlTypeLookupTest < ActiveRecord::PostgreSQLTestCase
|
|||
big_array = [123456789123456789]
|
||||
|
||||
assert_raises(ActiveModel::RangeError) { int_array.serialize(big_array) }
|
||||
assert_equal "{123456789123456789}", bigint_array.serialize(big_array)
|
||||
assert_equal "{123456789123456789}", @connection.type_cast(bigint_array.serialize(big_array))
|
||||
end
|
||||
|
||||
test "range types correctly respect registration of subtypes" do
|
||||
|
|
Loading…
Reference in a new issue