From 0d43aa1561384bc1f98182708fefd02ae0e69b65 Mon Sep 17 00:00:00 2001
From: "Aleksandar N. Kostadinov" <akostadi@redhat.com>
Date: Thu, 3 Feb 2022 19:22:49 +0200
Subject: [PATCH] raw type should be able to deserialize

ActiveRecord expects types to properly deserialize any values that
have been serialized. Right now (Rails 7.1) by every object save the
attributes are reset by serialize/deserialize cycle.

see ActiveModel::ActiveModel#forget_attribute_assignments
---
 lib/active_record/type/oracle_enhanced/raw.rb | 21 +++++++++-
 .../oracle_enhanced/type/raw_spec.rb          | 38 ++++++++++++++++++-
 2 files changed, 55 insertions(+), 4 deletions(-)

diff --git a/lib/active_record/type/oracle_enhanced/raw.rb b/lib/active_record/type/oracle_enhanced/raw.rb
index 947947ae1..0493830fc 100644
--- a/lib/active_record/type/oracle_enhanced/raw.rb
+++ b/lib/active_record/type/oracle_enhanced/raw.rb
@@ -10,14 +10,31 @@ def type
           :raw
         end
 
+        def deserialize(value)
+          value.is_a?(HEXData) ? value.raw_binary_string : super
+        end
+
         def serialize(value)
           # Encode a string or byte array as string of hex codes
           if value.nil?
             super
           else
-            value = value.unpack("C*")
-            value.map { |x| "%02X" % x }.join
+            HEXData.from_binary_string(value)
+          end
+        end
+
+        class HEXData < ::String
+          def self.from_binary_string(str)
+            new(str.unpack1("H*"))
+          end
+
+          def raw_binary_string
+            (0..length - 2).step(2).reduce(::String.new(capacity: length / 2, encoding: Encoding::BINARY)) do |data, i|
+              data << self[i, 2].hex
+            end
           end
+
+          OCI8::BindType::Mapping[self] = OCI8::BindType::String
         end
       end
     end
diff --git a/spec/active_record/oracle_enhanced/type/raw_spec.rb b/spec/active_record/oracle_enhanced/type/raw_spec.rb
index 4cd4d6812..a391e5d5f 100644
--- a/spec/active_record/oracle_enhanced/type/raw_spec.rb
+++ b/spec/active_record/oracle_enhanced/type/raw_spec.rb
@@ -38,6 +38,7 @@ class ::TestEmployee < ActiveRecord::Base
       last_name: "Last",
       binary_data: @binary_data
     )
+    expect(@employee.binary_data).to eq(@binary_data)
     @employee.reload
     expect(@employee.binary_data).to eq(@binary_data)
   end
@@ -51,6 +52,7 @@ class ::TestEmployee < ActiveRecord::Base
     expect(@employee.binary_data).to be_nil
     @employee.binary_data = @binary_data
     @employee.save!
+    expect(@employee.binary_data).to eq(@binary_data)
     @employee.reload
     expect(@employee.binary_data).to eq(@binary_data)
   end
@@ -77,6 +79,7 @@ class ::TestEmployee < ActiveRecord::Base
     @employee.reload
     @employee.binary_data = @binary_data2
     @employee.save!
+    expect(@employee.binary_data).to eq(@binary_data2)
     @employee.reload
     expect(@employee.binary_data).to eq(@binary_data2)
   end
@@ -116,13 +119,14 @@ class ::TestEmployee < ActiveRecord::Base
     @employee.reload
     @employee.binary_data = @binary_data
     @employee.save!
+    expect(@employee.binary_data).to eq(@binary_data)
     @employee.reload
     expect(@employee.binary_data).to eq(@binary_data)
   end
 
   it "should allow equality on select" do
     TestEmployee.delete_all
-    TestEmployee.create!(
+    employee = TestEmployee.create!(
       first_name: "First",
       last_name: "Last",
       binary_data: @binary_data,
@@ -132,6 +136,36 @@ class ::TestEmployee < ActiveRecord::Base
       last_name: "Last1",
       binary_data: @binary_data2,
     )
-    expect(TestEmployee.where(binary_data: @binary_data)).to have_attributes(count: 1)
+    expect(TestEmployee.where(binary_data: @binary_data).to_a).to eq([employee])
+  end
+
+  it "should allow equality on select with NULL value" do
+    TestEmployee.delete_all
+    employee = TestEmployee.create!(
+      first_name: "First",
+      last_name: "Last",
+    )
+    TestEmployee.create!(
+      first_name: "First1",
+      last_name: "Last1",
+      binary_data: @binary_data2,
+    )
+    expect(TestEmployee.where(binary_data: nil).to_a).to eq([employee])
+  end
+
+  it "should report changed when changed in place" do
+    employee = TestEmployee.create!(
+      first_name: "First",
+      last_name: "Last",
+      binary_data: @binary_data,
+    )
+    expect(employee.changed?).to be_falsey
+
+    employee.binary_data << "a"
+    expect(employee.changed?).to be(true)
+    expect(employee.changes).to eq({ "binary_data" => [@binary_data, @binary_data + "a"] })
+
+    employee.reload.binary_data << "b"
+    expect(employee.changes).to eq({ "binary_data" => [@binary_data, @binary_data + "b"] })
   end
 end