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:
Hubert Pompecki 2017-12-08 16:44:40 +00:00 committed by Jared Beck
parent 468c5cea48
commit 4cce9b0179
6 changed files with 130 additions and 2 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,3 @@
class PostgresUser < ActiveRecord::Base
has_paper_trail
end

View File

@ -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

View File

@ -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