Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue 015/serializing postgres arrays #1018

Merged
merged 11 commits into from
Dec 8, 2017
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
require "paper_trail/attribute_serializers/attribute_serializer_factory"

module PaperTrail
# :nodoc:
module AttributeSerializers
Expand Down Expand Up @@ -32,15 +34,15 @@ def defined_enums
# 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)
if defined_enums[attr] && val.is_a?(::String)
# 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
Expand Down
47 changes: 47 additions & 0 deletions lib/paper_trail/type_serializers/postgres_array_serializer.rb
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions spec/dummy_app/app/models/user.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class PostgresUser < ActiveRecord::Base
has_paper_trail
end
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,15 @@ def up
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like how you are testing two different types of arrays here. Also, it looks like this is the first test table to have arrays at all! Kind of surprising.

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
Expand Down
43 changes: 43 additions & 0 deletions spec/paper_trail/attribute_serializers/object_attribute_spec.rb
Original file line number Diff line number Diff line change
@@ -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