1
0
Fork 0
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:
Sean Griffin 2017-01-03 11:15:16 -05:00
parent 0d8069d365
commit 0f1d0b1b52
5 changed files with 62 additions and 17 deletions

View file

@ -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. * Optimize slow model instantiation when using STI and `store_full_sti_class = false` option.
*Konstantin Lazarev* *Konstantin Lazarev*

View file

@ -5,6 +5,8 @@ module ActiveRecord
class Array < Type::Value # :nodoc: class Array < Type::Value # :nodoc:
include Type::Helpers::Mutable include Type::Helpers::Mutable
Data = Struct.new(:encoder, :values) # :nodoc:
attr_reader :subtype, :delimiter attr_reader :subtype, :delimiter
delegate :type, :user_input_in_time_zone, :limit, :precision, :scale, to: :subtype delegate :type, :user_input_in_time_zone, :limit, :precision, :scale, to: :subtype
@ -17,8 +19,11 @@ module ActiveRecord
end end
def deserialize(value) def deserialize(value)
if value.is_a?(::String) case value
when ::String
type_cast_array(@pg_decoder.decode(value), :deserialize) type_cast_array(@pg_decoder.decode(value), :deserialize)
when Data
deserialize(value.values)
else else
super super
end end
@ -33,11 +38,8 @@ module ActiveRecord
def serialize(value) def serialize(value)
if value.is_a?(::Array) if value.is_a?(::Array)
result = @pg_encoder.encode(type_cast_array(value, :serialize)) casted_values = type_cast_array(value, :serialize)
if encoding = determine_encoding_of_strings(value) Data.new(@pg_encoder, casted_values)
result.force_encoding(encoding)
end
result
else else
super super
end end
@ -58,6 +60,10 @@ module ActiveRecord
value.map(&block) value.map(&block)
end end
def changed_in_place?(raw_old_value, new_value)
deserialize(raw_old_value) != new_value
end
private private
def type_cast_array(value, method) def type_cast_array(value, method)
@ -67,13 +73,6 @@ module ActiveRecord
@subtype.public_send(method, value) @subtype.public_send(method, value)
end end
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 end
end end

View file

@ -92,6 +92,8 @@ module ActiveRecord
else else
super super
end end
when OID::Array::Data
_quote(encode_array(value))
else else
super super
end end
@ -106,10 +108,37 @@ module ActiveRecord
{ value: value.to_s, format: 1 } { value: value.to_s, format: 1 }
when OID::Xml::Data, OID::Bit::Data when OID::Xml::Data, OID::Bit::Data
value.to_s value.to_s
when OID::Array::Data
encode_array(value)
else else
super super
end end
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 end
end end

View file

@ -21,6 +21,7 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase
t.datetime :datetimes, array: true t.datetime :datetimes, array: true
t.hstore :hstores, array: true t.hstore :hstores, array: true
t.decimal :decimals, array: true, default: [], precision: 10, scale: 2 t.decimal :decimals, array: true, default: [], precision: 10, scale: 2
t.timestamp :timestamps, array: true, default: [], precision: 6
end end
end end
PgArray.reset_column_information PgArray.reset_column_information
@ -213,7 +214,7 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase
x = PgArray.create!(tags: tags) x = PgArray.create!(tags: tags)
x.reload x.reload
assert_equal x.tags_before_type_cast, PgArray.type_for_attribute("tags").serialize(tags) refute x.changed?
end end
def test_quoting_non_standard_delimiters def test_quoting_non_standard_delimiters
@ -221,9 +222,10 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase
oid = ActiveRecord::ConnectionAdapters::PostgreSQL::OID oid = ActiveRecord::ConnectionAdapters::PostgreSQL::OID
comma_delim = oid::Array.new(ActiveRecord::Type::String.new, ",") comma_delim = oid::Array.new(ActiveRecord::Type::String.new, ",")
semicolon_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;}), conn.type_cast(comma_delim.serialize(strings))
assert_equal %({hello,;"world;"}), semicolon_delim.serialize(strings) assert_equal %({hello,;"world;"}), conn.type_cast(semicolon_delim.serialize(strings))
end end
def test_mutate_array 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])) assert_equal [arrays_of_utf8_strings], @type.deserialize(@type.serialize([arrays_of_utf8_strings]))
end 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 private
def assert_cycle(field, array) def assert_cycle(field, array)
# test creation # test creation

View file

@ -19,7 +19,7 @@ class PostgresqlTypeLookupTest < ActiveRecord::PostgreSQLTestCase
big_array = [123456789123456789] big_array = [123456789123456789]
assert_raises(ActiveModel::RangeError) { int_array.serialize(big_array) } 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 end
test "range types correctly respect registration of subtypes" do test "range types correctly respect registration of subtypes" do