Serializing postgres arrays (#1018)
This PR addresses #1015 Starting from Rails version 5.0.2 the default serializer of PostgreSQL columns returns an ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array object instead of a string. This new object is not suitable for JSON encoding and breaks versioning of any array fields backed by Postgres. Whenever a PostgreSQL array is used, instead of asking Active Record for a serializer we introduce our own, which simply returns the underlying array without any modifications.
This commit is contained in:
parent
468c5cea48
commit
4cce9b0179
|
@ -0,0 +1,24 @@
|
|||
require "paper_trail/type_serializers/postgres_array_serializer"
|
||||
|
||||
module PaperTrail
|
||||
module AttributeSerializers
|
||||
# Values returned by some Active Record serializers are
|
||||
# not suited for writing JSON to a text column. This factory
|
||||
# replaces certain default Active Record serializers
|
||||
# with custom PaperTrail ones.
|
||||
module AttributeSerializerFactory
|
||||
def self.for(klass, attr)
|
||||
active_record_serializer = klass.type_for_attribute(attr)
|
||||
case active_record_serializer
|
||||
when ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array then
|
||||
TypeSerializers::PostgresArraySerializer.new(
|
||||
active_record_serializer.subtype,
|
||||
active_record_serializer.delimiter
|
||||
)
|
||||
else
|
||||
active_record_serializer
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,3 +1,5 @@
|
|||
require "paper_trail/attribute_serializers/attribute_serializer_factory"
|
||||
|
||||
module PaperTrail
|
||||
# :nodoc:
|
||||
module AttributeSerializers
|
||||
|
@ -32,7 +34,7 @@ module PaperTrail
|
|||
# This implementation uses AR 5's `serialize` and `deserialize`.
|
||||
class CastAttributeSerializer
|
||||
def serialize(attr, val)
|
||||
@klass.type_for_attribute(attr).serialize(val)
|
||||
AttributeSerializerFactory.for(@klass, attr).serialize(val)
|
||||
end
|
||||
|
||||
def deserialize(attr, val)
|
||||
|
@ -40,7 +42,7 @@ module PaperTrail
|
|||
# Because PT 4 used to save the string version of enums to `object_changes`
|
||||
val
|
||||
else
|
||||
@klass.type_for_attribute(attr).deserialize(val)
|
||||
AttributeSerializerFactory.for(@klass, attr).deserialize(val)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
module PaperTrail
|
||||
module TypeSerializers
|
||||
# Provides an alternative method of serialization
|
||||
# and deserialization of PostgreSQL array columns.
|
||||
class PostgresArraySerializer
|
||||
def initialize(subtype, delimiter)
|
||||
@subtype = subtype
|
||||
@delimiter = delimiter
|
||||
end
|
||||
|
||||
def serialize(array)
|
||||
return serialize_with_ar(array) if active_record_pre_502?
|
||||
array
|
||||
end
|
||||
|
||||
def deserialize(array)
|
||||
return deserialize_with_ar(array) if active_record_pre_502?
|
||||
|
||||
case array
|
||||
# Needed for legacy reasons. If serialized array is a string
|
||||
# then it was serialized with Rails < 5.0.2.
|
||||
when ::String then deserialize_with_ar(array)
|
||||
else array
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def active_record_pre_502?
|
||||
::ActiveRecord::VERSION::MAJOR < 5 ||
|
||||
(::ActiveRecord::VERSION::MINOR.zero? && ::ActiveRecord::VERSION::TINY < 2)
|
||||
end
|
||||
|
||||
def serialize_with_ar(array)
|
||||
ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array.
|
||||
new(@subtype, @delimiter).
|
||||
serialize(array)
|
||||
end
|
||||
|
||||
def deserialize_with_ar(array)
|
||||
ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array.
|
||||
new(@subtype, @delimiter).
|
||||
deserialize(array)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,3 @@
|
|||
class PostgresUser < ActiveRecord::Base
|
||||
has_paper_trail
|
||||
end
|
|
@ -61,6 +61,15 @@ class SetUpTestTables < (
|
|||
t.timestamps null: true
|
||||
end
|
||||
|
||||
if ENV["DB"] == "postgres"
|
||||
create_table :postgres_users, force: true do |t|
|
||||
t.string :name
|
||||
t.integer :post_ids, array: true
|
||||
t.datetime :login_times, array: true
|
||||
t.timestamps null: true
|
||||
end
|
||||
end
|
||||
|
||||
create_table :versions, versions_table_options do |t|
|
||||
t.string :item_type, item_type_options
|
||||
t.integer :item_id, null: false
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
require "spec_helper"
|
||||
|
||||
module PaperTrail
|
||||
module AttributeSerializers
|
||||
::RSpec.describe ObjectAttribute do
|
||||
if ENV["DB"] == "postgres" && ::ActiveRecord::VERSION::MAJOR >= 5
|
||||
describe "postgres-specific column types" do
|
||||
describe "#serialize" do
|
||||
it "serializes a postgres array into a plain array" do
|
||||
attrs = { "post_ids" => [1, 2, 3] }
|
||||
described_class.new(PostgresUser).serialize(attrs)
|
||||
expect(attrs["post_ids"]).to eq [1, 2, 3]
|
||||
end
|
||||
end
|
||||
|
||||
describe "#deserialize" do
|
||||
it "deserializes a plain array correctly" do
|
||||
attrs = { "post_ids" => [1, 2, 3] }
|
||||
described_class.new(PostgresUser).deserialize(attrs)
|
||||
expect(attrs["post_ids"]).to eq [1, 2, 3]
|
||||
end
|
||||
|
||||
it "deserializes an array serialized with Rails <= 5.0.1 correctly" do
|
||||
attrs = { "post_ids" => "{1,2,3}" }
|
||||
described_class.new(PostgresUser).deserialize(attrs)
|
||||
expect(attrs["post_ids"]).to eq [1, 2, 3]
|
||||
end
|
||||
|
||||
it "deserializes an array of time objects correctly" do
|
||||
date1 = 1.day.ago
|
||||
date2 = 2.days.ago
|
||||
date3 = 3.days.ago
|
||||
attrs = { "post_ids" => [date1, date2, date3] }
|
||||
described_class.new(PostgresUser).serialize(attrs)
|
||||
described_class.new(PostgresUser).deserialize(attrs)
|
||||
expect(attrs["post_ids"]).to eq [date1, date2, date3]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue