Skip to content

Commit

Permalink
Add Postgres Range Serializer support
Browse files Browse the repository at this point in the history
  • Loading branch information
sirwolfgang committed Aug 19, 2024
1 parent 128ee1a commit 416109b
Show file tree
Hide file tree
Showing 5 changed files with 106 additions and 9 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# frozen_string_literal: true

require "paper_trail/type_serializers/postgres_array_serializer"
require "paper_trail/type_serializers/postgres_range_serializer"

module PaperTrail
module AttributeSerializers
Expand All @@ -15,11 +16,16 @@ class << self
# @api private
def for(model_class, attr)
active_record_serializer = model_class.type_for_attribute(attr)

if ar_pg_array?(active_record_serializer)
TypeSerializers::PostgresArraySerializer.new(
active_record_serializer.subtype,
active_record_serializer.delimiter
)
elsif ar_pg_range?(active_record_serializer)
TypeSerializers::PostgresRangeSerializer.new(
active_record_serializer
)
else
active_record_serializer
end
Expand All @@ -35,6 +41,15 @@ def ar_pg_array?(obj)
false
end
end

# @api private
def ar_pg_range?(obj)
if defined?(::ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Range)
obj.instance_of?(::ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Range)
else
false
end
end
end
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ def initialize(model_class)
# ActiveRecord::Enum was added in AR 4.1
# http://edgeguides.rubyonrails.org/4_1_release_notes.html#active-record-enums
def defined_enums
@defined_enums ||= (@model_class.respond_to?(:defined_enums) ? @model_class.defined_enums : {})
@defined_enums ||=
@model_class.respond_to?(:defined_enums) ? @model_class.defined_enums : {}
end

def deserialize(attr, val)
Expand Down
24 changes: 20 additions & 4 deletions lib/paper_trail/attribute_serializers/object_attribute.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# frozen_string_literal: true

require "paper_trail/attribute_serializers/cast_attribute_serializer"
require "paper_trail/type_serializers/postgres_range_serializer"

module PaperTrail
module AttributeSerializers
Expand Down Expand Up @@ -29,10 +30,7 @@ def deserialize(attributes)
# Modifies `attributes` in place.
# TODO: Return a new hash instead.
def alter(attributes, serialization_method)
# Don't serialize non-encrypted before values before inserting into columns of type
# `JSON` on `PostgreSQL` databases.
attributes_to_serialize =
object_col_is_json? ? attributes.slice(*@encrypted_attributes) : attributes
attributes_to_serialize = attributes_to_serialize(attributes)
return attributes if attributes_to_serialize.blank?

serializer = CastAttributeSerializer.new(@model_class)
Expand All @@ -43,6 +41,24 @@ def alter(attributes, serialization_method)
attributes
end

# Don't de/serialize non-encrypted before values before inserting into columns of type
# `JSON` on `PostgreSQL` databases; Unless it's a special type like a range.
def attributes_to_serialize(attributes)
encrypted_to_serialize = if object_col_is_json?
attributes.slice(*@encrypted_attributes)
else
attributes
end

columns_to_serialize = attributes.select { |column, _|
TypeSerializers::PostgresRangeSerializer.range_type?(
@model_class.columns_hash[column]&.type
)
}

encrypted_to_serialize.merge(columns_to_serialize)
end

def object_col_is_json?
@model_class.paper_trail.version_class.object_col_is_json?
end
Expand Down
24 changes: 20 additions & 4 deletions lib/paper_trail/attribute_serializers/object_changes_attribute.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# frozen_string_literal: true

require "paper_trail/attribute_serializers/cast_attribute_serializer"
require "paper_trail/type_serializers/postgres_range_serializer"

module PaperTrail
module AttributeSerializers
Expand Down Expand Up @@ -29,10 +30,7 @@ def deserialize(changes)
# Modifies `changes` in place.
# TODO: Return a new hash instead.
def alter(changes, serialization_method)
# Don't serialize non-encrypted before values before inserting into columns of type
# `JSON` on `PostgreSQL` databases.
changes_to_serialize =
object_changes_col_is_json? ? changes.slice(*@encrypted_attributes) : changes.clone
changes_to_serialize = changes_to_serialize(changes)
return changes if changes_to_serialize.blank?

serializer = CastAttributeSerializer.new(@model_class)
Expand All @@ -46,6 +44,24 @@ def alter(changes, serialization_method)
changes
end

# Don't de/serialize non-encrypted before values before inserting into columns of type
# `JSON` on `PostgreSQL` databases; Unless it's a special type like a range.
def changes_to_serialize(changes)
encrypted_to_serialize = if object_changes_col_is_json?
changes.slice(*@encrypted_attributes)
else
changes.clone
end

columns_to_serialize = changes.select { |column, _|
TypeSerializers::PostgresRangeSerializer.range_type?(
@model_class.columns_hash[column]&.type
)
}

encrypted_to_serialize.merge(columns_to_serialize)
end

def object_changes_col_is_json?
@model_class.paper_trail.version_class.object_changes_col_is_json?
end
Expand Down
49 changes: 49 additions & 0 deletions lib/paper_trail/type_serializers/postgres_range_serializer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# frozen_string_literal: true

module PaperTrail
module TypeSerializers
# Provides an alternative method of serialization
# and deserialization of PostgreSQL range columns.
class PostgresRangeSerializer
# @see https://github.com/rails/rails/blob/main/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb#L147-L152
RANGE_TYPES = %i[
daterange
numrange
tsrange
tstzrange
int4range
int8range
].freeze

def self.range_type?(type)
RANGE_TYPES.include?(type)
end

def initialize(active_record_serializer)
@active_record_serializer = active_record_serializer
end

def serialize(range)
range
end

def deserialize(range)
range.is_a?(String) ? deserialize_with_ar(range) : range
end

private

def deserialize_with_ar(string)
return nil if string.blank?

delimiter = string[/\.{2,3}/]
range_start, range_end = string.split(delimiter)

range_start = @active_record_serializer.subtype.cast(range_start)
range_end = @active_record_serializer.subtype.cast(range_end)

Range.new(range_start, range_end, exclude_end: delimiter == "...")
end
end
end
end

0 comments on commit 416109b

Please sign in to comment.