Skip to content

Commit

Permalink
Add postgres range serializer
Browse files Browse the repository at this point in the history
  • Loading branch information
sirwolfgang committed Aug 18, 2024
1 parent 128ee1a commit 3414859
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 8 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
15 changes: 11 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,15 @@ 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 = object_col_is_json? ? attributes.slice(*@encrypted_attributes) : attributes
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
15 changes: 11 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,15 @@ 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 = object_changes_col_is_json? ? changes.slice(*@encrypted_attributes) : changes.clone
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 = [
:daterange,
:numrange,
:tsrange,
:tstzrange,
:int4range,
:int8range
]

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 3414859

Please sign in to comment.