Last active
October 20, 2021 22:10
-
-
Save chrisbloom7/7aeae0d0c25cb4fbc2f9b2e4117bcb93 to your computer and use it in GitHub Desktop.
Revisions
-
Chris Bloom revised this gist
Oct 20, 2021 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -133,7 +133,7 @@ def test_audited_serialized_skipped_blob_column_FAILS # changes after saving when the field contains unicode characters, even if # the changed fields did not include the blob column. # # THIS TEST FAILS def test_audited_serialized_blob_column_when_other_column_is_updated_FAILS assert_changes_empty AuditedSerializedReview.create!(blob: { "a" => UNICODE_CHAR }) do |record| record.text = "c" -
Chris Bloom revised this gist
Oct 20, 2021 . 1 changed file with 17 additions and 18 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -89,25 +89,25 @@ class ReviewTest < ActiveSupport::TestCase # # THESE TESTS FAIL def test_audited_serialized_tinyblob_column_FAILS assert_changes_empty AuditedSerializedReview.create!(tinyblob: { "a" => UNICODE_CHAR }) do |record| record.tinyblob["b"] = "c" end end def test_audited_serialized_blob_column_FAILS assert_changes_empty AuditedSerializedReview.create!(blob: { "a" => UNICODE_CHAR }) do |record| record.blob["b"] = "c" end end def test_audited_serialized_mediumblob_column_FAILS assert_changes_empty AuditedSerializedReview.create!(mediumblob: { "a" => UNICODE_CHAR }) do |record| record.mediumblob["b"] = "c" end end def test_audited_serialized_longblob_column_FAILS assert_changes_empty AuditedSerializedReview.create!(longblob: { "a" => UNICODE_CHAR }) do |record| record.longblob["b"] = "c" end end @@ -118,13 +118,13 @@ def test_audited_serialized_longblob_column_FAILS # # THESE TESTS FAIL def test_audited_serialized_ignored_blob_column_FAILS assert_changes_empty AuditedSerializedReview.create!(ignored_blob: { "a" => UNICODE_CHAR }) do |record| record.ignored_blob["b"] = "c" end end def test_audited_serialized_skipped_blob_column_FAILS assert_changes_empty AuditedSerializedReview.create!(skipped_blob: { "a" => UNICODE_CHAR }) do |record| record.skipped_blob["b"] = "c" end end @@ -135,7 +135,7 @@ def test_audited_serialized_skipped_blob_column_FAILS # # THESE TESTS FAIL def test_audited_serialized_blob_column_when_other_column_is_updated_FAILS assert_changes_empty AuditedSerializedReview.create!(blob: { "a" => UNICODE_CHAR }) do |record| record.text = "c" end end @@ -147,7 +147,7 @@ def test_audited_serialized_blob_column_when_other_column_is_updated_FAILS # THIS TEST PASSES def test_audited_serialized_blob_column_paper_trail_disabled_PASSES PaperTrail.enabled = false assert_changes_empty AuditedSerializedReview.create!(blob: { "a" => UNICODE_CHAR }) do |record| record.blob["b"] = "c" end end @@ -158,7 +158,7 @@ def test_audited_serialized_blob_column_paper_trail_disabled_PASSES # # THIS TEST PASSES def test_reloaded_audited_serialized_blob_column_PASSES record = assert_changes_empty AuditedSerializedReview.create!(blob: { "a" => UNICODE_CHAR }), reload_after_update: true do |record| record.blob["b"] = "c" end assert_equal record.blob["a"], "\u2022" @@ -170,25 +170,25 @@ def test_reloaded_audited_serialized_blob_column_PASSES # # THESE TESTS PASS def test_audited_serialized_tinytext_column_PASSES assert_changes_empty AuditedSerializedReview.create!(tinytext: { "a" => UNICODE_CHAR }) do |record| record.tinytext["b"] = "c" end end def test_audited_serialized_text_column_PASSES assert_changes_empty AuditedSerializedReview.create!(text: { "a" => UNICODE_CHAR }) do |record| record.text["b"] = "c" end end def test_audited_serialized_mediumtext_column_PASSES assert_changes_empty AuditedSerializedReview.create!(mediumtext: { "a" => UNICODE_CHAR }) do |record| record.mediumtext["b"] = "c" end end def test_audited_serialized_longtext_column_PASSES assert_changes_empty AuditedSerializedReview.create!(longtext: { "a" => UNICODE_CHAR }) do |record| record.longtext["b"] = "c" end end @@ -198,7 +198,7 @@ def test_audited_serialized_longtext_column_PASSES # # THIS TEST PASSES def test_unaudited_serialized_blob_column_PASSES assert_changes_empty SerializedReview.create!(blob: { "a" => UNICODE_CHAR }) do |record| record.blob["b"] = "c" end end @@ -208,7 +208,7 @@ def test_unaudited_serialized_blob_column_PASSES # # THIS TEST PASSES def test_audited_serialized_blob_column_without_unicode_PASSES assert_changes_empty AuditedSerializedReview.create!(blob: { "a" => "not unicode" }) do |record| record.blob["b"] = "c" end end @@ -218,16 +218,15 @@ def test_audited_serialized_blob_column_without_unicode_PASSES # # THIS TEST PASSES def test_audited_unserialized_blob_column_PASSES assert_changes_empty AuditedReview.create!(blob: UNICODE_CHAR) do |record| record.blob = "c" end end def assert_changes_empty(record, options = {}) reload_after_update = options.delete(:reload_after_update) || false # Changes are empty after creation assert_empty record.changes # Changes are populated after updating serialized column -
Chris Bloom renamed this gist
Oct 20, 2021 . 1 changed file with 0 additions and 0 deletions.There are no files selected for viewing
File renamed without changes. -
Chris Bloom revised this gist
Oct 20, 2021 . 2 changed files with 190 additions and 99 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,44 @@ ReviewTest test_audited_serialized_blob_column_FAILS FAIL (0.09s) Expected {"blob"=>[{"a"=>"•", "b"=>"c"}, {"a"=>"•", "b"=>"c"}]} to be empty. /Users/chrisbloom7/src/sandbox/rails-serialize-unicode-test.rb:238:in `assert_changes_empty' /Users/chrisbloom7/src/sandbox/rails-serialize-unicode-test.rb:96:in `test_audited_serialized_blob_column_FAILS' test_audited_serialized_tinyblob_column_FAILS FAIL (0.05s) Expected {"tinyblob"=>[{"a"=>"•", "b"=>"c"}, {"a"=>"•", "b"=>"c"}]} to be empty. /Users/chrisbloom7/src/sandbox/rails-serialize-unicode-test.rb:238:in `assert_changes_empty' /Users/chrisbloom7/src/sandbox/rails-serialize-unicode-test.rb:90:in `test_audited_serialized_tinyblob_column_FAILS' test_audited_serialized_mediumtext_column_PASSES PASS (0.05s) test_audited_serialized_blob_column_without_unicode_PASSES PASS (0.05s) test_audited_serialized_longtext_column_PASSES PASS (0.05s) test_reloaded_audited_serialized_blob_column_PASSES PASS (0.06s) test_audited_serialized_blob_column_when_other_column_is_updated_FAILS FAIL (0.05s) Expected {"blob"=>[{"a"=>"•"}, {"a"=>"•"}]} to be empty. /Users/chrisbloom7/src/sandbox/rails-serialize-unicode-test.rb:238:in `assert_changes_empty' /Users/chrisbloom7/src/sandbox/rails-serialize-unicode-test.rb:136:in `test_audited_serialized_blob_column_when_other_column_is_updated_FAILS' test_audited_serialized_text_column_PASSES PASS (0.05s) test_audited_serialized_tinytext_column_PASSES PASS (0.05s) test_audited_unserialized_blob_column_PASSES PASS (0.06s) test_audited_serialized_blob_column_paper_trail_disabled_PASSES PASS (0.05s) test_audited_serialized_mediumblob_column_FAILS FAIL (0.05s) Expected {"mediumblob"=>[{"a"=>"•", "b"=>"c"}, {"a"=>"•", "b"=>"c"}]} to be empty. /Users/chrisbloom7/src/sandbox/rails-serialize-unicode-test.rb:238:in `assert_changes_empty' /Users/chrisbloom7/src/sandbox/rails-serialize-unicode-test.rb:102:in `test_audited_serialized_mediumblob_column_FAILS' test_audited_serialized_ignored_blob_column_FAILS FAIL (0.05s) Expected {"ignored_blob"=>[{"a"=>"•", "b"=>"c"}, {"a"=>"•", "b"=>"c"}]} to be empty. /Users/chrisbloom7/src/sandbox/rails-serialize-unicode-test.rb:238:in `assert_changes_empty' /Users/chrisbloom7/src/sandbox/rails-serialize-unicode-test.rb:119:in `test_audited_serialized_ignored_blob_column_FAILS' test_unaudited_serialized_blob_column_PASSES PASS (0.05s) test_audited_serialized_skipped_blob_column_FAILS FAIL (0.05s) Expected {"skipped_blob"=>[{"a"=>"•", "b"=>"c"}, {"a"=>"•", "b"=>"c"}]} to be empty. /Users/chrisbloom7/src/sandbox/rails-serialize-unicode-test.rb:238:in `assert_changes_empty' /Users/chrisbloom7/src/sandbox/rails-serialize-unicode-test.rb:125:in `test_audited_serialized_skipped_blob_column_FAILS' test_audited_serialized_longblob_column_FAILS FAIL (0.05s) Expected {"longblob"=>[{"a"=>"•", "b"=>"c"}, {"a"=>"•", "b"=>"c"}]} to be empty. /Users/chrisbloom7/src/sandbox/rails-serialize-unicode-test.rb:238:in `assert_changes_empty' /Users/chrisbloom7/src/sandbox/rails-serialize-unicode-test.rb:108:in `test_audited_serialized_longblob_column_FAILS' This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,4 +1,4 @@ # frozen_string_literal: true require "bundler/inline" @@ -8,6 +8,7 @@ gem "mysql2", "~> 0.5.3" gem "paper_trail", "~> 12.1.0" gem "minitest-reporters" gem "pry-byebug" end require "active_record" @@ -19,11 +20,19 @@ ActiveRecord::Base.logger = nil ActiveRecord::Schema.define do create_table :reviews, force: true do |t| # blob columns are not a default Rails column type, but Rails will defer to the # mysql2 adapter to manage the mapping t.tinyblob :tinyblob t.blob :blob t.mediumblob :mediumblob t.longblob :longblob t.blob :ignored_blob t.blob :skipped_blob t.text :tinytext t.text :text t.text :mediumtext t.longtext :longtext end create_table :versions, force: true do |t| @@ -44,13 +53,22 @@ class Review < ActiveRecord::Base; end class SerializedReview < Review # Use default YAML serialization serialize :tinyblob serialize :blob serialize :mediumblob serialize :longblob serialize :tinytext serialize :text serialize :mediumtext serialize :longtext serialize :ignored_blob serialize :skipped_blob end class AuditedReview < Review has_paper_trail ignore: [:ignored_blob], skip: [:skipped_blob] end class AuditedSerializedReview < SerializedReview @@ -60,138 +78,167 @@ class AuditedSerializedReview < SerializedReview Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new class ReviewTest < ActiveSupport::TestCase setup do PaperTrail.enabled = true end UNICODE_CHAR = "\u2022" # Serialized blob columns that use paper trail will not reset changes # after saving when the field contains unicode characters # # THESE TESTS FAIL def test_audited_serialized_tinyblob_column_FAILS assert_changes_empty AuditedSerializedReview, { tinyblob: { "a" => UNICODE_CHAR } } do |record| record.tinyblob["b"] = "c" end end def test_audited_serialized_blob_column_FAILS assert_changes_empty AuditedSerializedReview, { blob: { "a" => UNICODE_CHAR } } do |record| record.blob["b"] = "c" end end def test_audited_serialized_mediumblob_column_FAILS assert_changes_empty AuditedSerializedReview, { mediumblob: { "a" => UNICODE_CHAR } } do |record| record.mediumblob["b"] = "c" end end def test_audited_serialized_longblob_column_FAILS assert_changes_empty AuditedSerializedReview, { longblob: { "a" => UNICODE_CHAR } } do |record| record.longblob["b"] = "c" end end # Serialized blob columns on classes that use paper trail will not reset # changes after saving when the field contains unicode characters, even if # the attribute is ignored or skipped by paper trail. # # THESE TESTS FAIL def test_audited_serialized_ignored_blob_column_FAILS assert_changes_empty AuditedSerializedReview, { ignored_blob: { "a" => UNICODE_CHAR } } do |record| record.ignored_blob["b"] = "c" end end def test_audited_serialized_skipped_blob_column_FAILS assert_changes_empty AuditedSerializedReview, { skipped_blob: { "a" => UNICODE_CHAR } } do |record| record.skipped_blob["b"] = "c" end end # Serialized blob columns on classes that use paper trail will not reset # changes after saving when the field contains unicode characters, even if # the changed fields did not include the blob column. # # THESE TESTS FAIL def test_audited_serialized_blob_column_when_other_column_is_updated_FAILS assert_changes_empty AuditedSerializedReview, { blob: { "a" => UNICODE_CHAR } } do |record| record.text = "c" end end # Serialized blob columns that use paper trail WILL reset changes # after saving when the field contains unicode characters and PaperTrail # is DISABLED # # THIS TEST PASSES def test_audited_serialized_blob_column_paper_trail_disabled_PASSES PaperTrail.enabled = false assert_changes_empty AuditedSerializedReview, { blob: { "a" => UNICODE_CHAR } } do |record| record.blob["b"] = "c" end end # Serialized blob columns that use paper trail will not reset changes # after saving when the field contains unicode characters, BUT reloading # the record clears them and loads updated field content. # # THIS TEST PASSES def test_reloaded_audited_serialized_blob_column_PASSES record = assert_changes_empty AuditedSerializedReview, { blob: { "a" => UNICODE_CHAR } }, reload_after_update: true do |record| record.blob["b"] = "c" end assert_equal record.blob["a"], "\u2022" assert_equal record.blob["b"], "c" end # Serialized _text_ columns that use paper trail WILL reset changes # after saving when the field contains unicode characters # # THESE TESTS PASS def test_audited_serialized_tinytext_column_PASSES assert_changes_empty AuditedSerializedReview, { tinytext: { "a" => UNICODE_CHAR } } do |record| record.tinytext["b"] = "c" end end def test_audited_serialized_text_column_PASSES assert_changes_empty AuditedSerializedReview, { text: { "a" => UNICODE_CHAR } } do |record| record.text["b"] = "c" end end def test_audited_serialized_mediumtext_column_PASSES assert_changes_empty AuditedSerializedReview, { mediumtext: { "a" => UNICODE_CHAR } } do |record| record.mediumtext["b"] = "c" end end def test_audited_serialized_longtext_column_PASSES assert_changes_empty AuditedSerializedReview, { longtext: { "a" => UNICODE_CHAR } } do |record| record.longtext["b"] = "c" end end # Serialized blob columns that DO NOT use paper trail WILL reset changes # after saving when the field contains unicode characters # # THIS TEST PASSES def test_unaudited_serialized_blob_column_PASSES assert_changes_empty SerializedReview, { blob: { "a" => UNICODE_CHAR } } do |record| record.blob["b"] = "c" end end # Serialized blob columns that use paper trail WILL reset changes # after saving when the field DOES NOT contain unicode characters # # THIS TEST PASSES def test_audited_serialized_blob_column_without_unicode_PASSES assert_changes_empty AuditedSerializedReview, { blob: { "a" => "not unicode" } } do |record| record.blob["b"] = "c" end end # UN-serialized blob columns that use paper trail WILL reset changes # after saving when the field contains unicode characters # # THIS TEST PASSES def test_audited_unserialized_blob_column_PASSES assert_changes_empty AuditedReview, { blob: UNICODE_CHAR } do |record| record.blob = "c" end end def assert_changes_empty(klass, starting_attributes, options = {}) reload_after_update = options.delete(:reload_after_update) || false # Changes are empty after creation record = klass.create!(starting_attributes) assert_empty record.changes # Changes are populated after updating serialized column yield(record) refute_empty record.changes record.save! # This should have been reset after `save!` and should be empty record.reload if reload_after_update assert_empty record.changes record end end -
Chris Bloom revised this gist
Oct 14, 2021 . 1 changed file with 11 additions and 0 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,11 @@ I've found a bug that is specific to the following scenario: - Using MySQL - Using a `longblob` column type - Using a [serialized attribute](https://api.rubyonrails.org/classes/ActiveModel/Serialization.html) - Using `paper_trail` gem - Serialized field contains unicode character Under those conditions, after saving changes to the serialized field, `record#changed?` still reports true, and `record#changes` contains an entry for the serialized field where both the before and after elements are identical. Calling `record#reload` clears the changes and loads the record with the changed value. If `paper_trail` is removed from the scenario, the ActiveModel [attribute mutation tracking](https://github.com/rails/rails/blob/v6.1.4.1/activemodel/lib/active_model/attribute_mutation_tracker.rb) works as expected. Documented in https://github.com/paper-trail-gem/paper_trail/issues/1348 -
Chris Bloom revised this gist
Oct 14, 2021 . 2 changed files with 48 additions and 2 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -22,6 +22,7 @@ # longblob is not a default Rails column type, but Rails will defer to the # mysql2 adapter to manage the mapping t.longblob :longblob t.longtext :longtext t.text :text end @@ -44,6 +45,7 @@ class Review < ActiveRecord::Base; end class SerializedReview < Review # Use default YAML serialization serialize :longblob serialize :longtext serialize :text end @@ -77,6 +79,49 @@ def test_audited_serialized_longblob_column_FAILS assert_empty review.changes end # Serialized longblob columns that use paper trail will not reset changes # after saving when the field contains unicode characters, BUT reloading # the record clears them and loads updated field content. # # THIS TEST PASSES def test_reloaded_audited_serialized_longblob_column_PASSES review = AuditedSerializedReview.create!(longblob: { "a" => "\u2022" }) # Changes are empty after creation assert_empty review.changes # Changes are populated after updating serialized column review.longblob["b"] = "c" refute_empty review.changes review.save! # This should have been reset after `save!` and should be empty, but isn't # until after it is reloaded refute_empty review.changes assert_empty review.reload.changes assert_equal review.longblob["a"], "\u2022" assert_equal review.longblob["b"], "c" end # Serialized longTEXT columns that use paper trail WILL reset changes # after saving when the field contains unicode characters # # THIS TEST PASSES def test_audited_serialized_longtext_column_PASSES review = AuditedSerializedReview.create!(longtext: { "a" => "\u2022" }) # Changes are empty after creation assert_empty review.changes # Changes are populated after updating serialized column review.longtext["b"] = "c" refute_empty review.changes review.save! # This should have been reset after `save!` and should be empty assert_empty review.changes end # Serialized longblob columns that DO NOT use paper trail WILL reset changes # after saving when the field contains unicode characters # This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -15,9 +15,10 @@ CREATE TABLE `ar_internal_metadata` ( CREATE TABLE `reviews` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `longblob` longblob, `longtext` longtext, `text` text, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; @@ -32,5 +33,5 @@ CREATE TABLE `versions` ( `created_at` datetime DEFAULT NULL, PRIMARY KEY (`id`), KEY `index_versions_on_item_type_and_item_id` (`item_type`,`item_id`) ) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; -
Chris Bloom revised this gist
Oct 14, 2021 . 3 changed files with 163 additions and 65 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,55 +0,0 @@ This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,152 @@ # Run with `ruby rails-serialize-unicode-test.rb` require "bundler/inline" gemfile true do source "https://rubygems.org" gem "activerecord", "~> 6.1.4.1" gem "mysql2", "~> 0.5.3" gem "paper_trail", "~> 12.1.0" gem "minitest-reporters" end require "active_record" require "paper_trail" require "minitest/autorun" require "logger" ActiveRecord::Base.establish_connection(adapter: "mysql2", database: "railstestdb") ActiveRecord::Base.logger = nil ActiveRecord::Schema.define do create_table :reviews, force: true do |t| # longblob is not a default Rails column type, but Rails will defer to the # mysql2 adapter to manage the mapping t.longblob :longblob t.text :text end create_table :versions, force: true do |t| t.string :item_type, null: false t.integer :item_id, null: false t.string :event, null: false t.string :whodunnit t.text :object, limit: 1_073_741_823 t.text :object_changes, limit: 1_073_741_823 t.datetime :created_at end add_index :versions, %i[item_type item_id] end # ActiveRecord::Base.logger = Logger.new(STDOUT) class Review < ActiveRecord::Base; end class SerializedReview < Review # Use default YAML serialization serialize :longblob serialize :text end class AuditedReview < Review has_paper_trail end class AuditedSerializedReview < SerializedReview has_paper_trail end Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new class ReviewTest < ActiveSupport::TestCase # Serialized longblob columns that use paper trail will not reset changes # after saving when the field contains unicode characters # # THIS TEST FAILS def test_audited_serialized_longblob_column_FAILS review = AuditedSerializedReview.create!(longblob: { "a" => "\u2022" }) # Changes are empty after creation assert_empty review.changes # Changes are populated after updating serialized column review.longblob["b"] = "c" refute_empty review.changes review.save! # This should have been reset after `save!` and should be empty assert_empty review.changes end # Serialized longblob columns that DO NOT use paper trail WILL reset changes # after saving when the field contains unicode characters # # THIS TEST PASSES def test_unaudited_serialized_longblob_column_PASSES review = SerializedReview.create!(longblob: { "a" => "\u2022" }) # Changes are empty after creation assert_empty review.changes # Changes are populated after updating serialized column review.longblob["b"] = "c" refute_empty review.changes review.save! # This should have been reset after `save!` and should be empty assert_empty review.changes end # Serialized _text_ columns that use paper trail WILL reset changes # after saving when the field contains unicode characters # # THIS TEST PASSES def test_audited_serialized_text_column_PASSES review = AuditedSerializedReview.create!(text: { "a" => "\u2022" }) # Changes are empty after creation assert_empty review.changes # Changes are populated after updating serialized column review.text["b"] = "c" refute_empty review.changes review.save! # This should have been reset after `save!` and should be empty assert_empty review.changes end # Serialized longblob columns that use paper trail WILL reset changes # after saving when the field DOES NOT contain unicode characters # # THIS TEST PASSES def test_audited_serialized_longblob_column_without_unicode_PASSES review = AuditedSerializedReview.create!(longblob: { "a" => "2022" }) # Changes are empty after creation assert_empty review.changes # Changes are populated after updating serialized column review.longblob["b"] = "c" refute_empty review.changes review.save! # This should have been reset after `save!` and should be empty assert_empty review.changes end # UNserialized longblob columns that use paper trail WILL reset changes # after saving when the field contains unicode characters # # THIS TEST PASSES def test_audited_unserialized_longblob_column_PASSES review = AuditedReview.create!(longblob: "\u2022") assert_empty review.changes review.longblob = "something else" refute_empty review.changes review.save! # This should have been reset after `save!` assert_empty review.changes end end This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,13 +1,5 @@ -- $ mysqldump --compact=ON --set-gtid-purged=OFF --no-data=ON railstestdb /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `ar_internal_metadata` ( @@ -20,9 +12,18 @@ CREATE TABLE `ar_internal_metadata` ( /*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `reviews` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `longblob` longblob, `text` text, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `versions` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `item_type` varchar(255) NOT NULL, `item_id` int(11) NOT NULL, `event` varchar(255) NOT NULL, `whodunnit` varchar(255) DEFAULT NULL, @@ -31,5 +32,5 @@ CREATE TABLE `versions` ( `created_at` datetime DEFAULT NULL, PRIMARY KEY (`id`), KEY `index_versions_on_item_type_and_item_id` (`item_type`,`item_id`) ) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; -
Chris Bloom revised this gist
Oct 14, 2021 . 2 changed files with 0 additions and 0 deletions.There are no files selected for viewing
File renamed without changes.File renamed without changes. -
Chris Bloom revised this gist
Oct 14, 2021 . 2 changed files with 36 additions and 93 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,4 +1,4 @@ -- $ mysqldump --compact=ON --set-gtid-purged=OFF --no-data=ON railstestdb /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; @@ -17,4 +17,19 @@ CREATE TABLE `ar_internal_metadata` ( `updated_at` datetime(6) NOT NULL, PRIMARY KEY (`key`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `versions` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `item_type` varchar(191) NOT NULL, `item_id` int(11) NOT NULL, `event` varchar(255) NOT NULL, `whodunnit` varchar(255) DEFAULT NULL, `object` longtext, `object_changes` longtext, `created_at` datetime DEFAULT NULL, PRIMARY KEY (`id`), KEY `index_versions_on_item_type_and_item_id` (`item_type`,`item_id`) ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -3,14 +3,16 @@ require "bundler/inline" gemfile true do gem "rails", "~> 6.1.4.1" gem "mysql2", "~> 0.5.3" gem "paper_trail", "~> 12.1.0" gem "pry-byebug" end require "active_record" require "paper_trail" require "paper_trail/frameworks/active_record" require "minitest/autorun" ActiveRecord::Base.establish_connection(adapter: "mysql2", database: "railstestdb") ActiveRecord::Base.logger = Logger.new(STDOUT) @@ -19,106 +21,32 @@ create_table :advisory_reviews, force: true do |t| t.longblob :advisory_payload, null: false end create_table :versions, force: true do |t| t.string :item_type, null: false, limit: 191 t.integer :item_id, null: false t.string :event, null: false t.string :whodunnit t.text :object, limit: 1_073_741_823 t.text :object_changes, limit: 1_073_741_823 t.datetime :created_at end add_index :versions, %i[item_type item_id] end class AdvisoryReview < ActiveRecord::Base has_paper_trail serialize :advisory_payload end class AdvisoryReviewTest < Minitest::Test def test_unicode_changes advisory_review = AdvisoryReview.create!(advisory_payload: { "description" => "\u2022" }) assert_empty advisory_review.changes advisory_review.advisory_payload["not_the_description"] = "foo" refute_empty advisory_review.changes advisory_review.save! # This should have been reset after `save!` -
Chris Bloom revised this gist
Oct 14, 2021 . 1 changed file with 1 addition and 3 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -8,7 +8,6 @@ CREATE TABLE `advisory_reviews` ( PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `ar_internal_metadata` ( @@ -18,5 +17,4 @@ CREATE TABLE `ar_internal_metadata` ( `updated_at` datetime(6) NOT NULL, PRIMARY KEY (`key`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; -
Chris Bloom revised this gist
Oct 14, 2021 . 2 changed files with 22 additions and 71 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,22 @@ -- $ mysqldump --no-data railstestdb > dump.sql /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `advisory_reviews` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `advisory_payload` longblob NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; INSERT INTO `advisory_reviews` VALUES (1,_binary '---\ndescription: n’t\n'); /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `ar_internal_metadata` ( `key` varchar(255) NOT NULL, `value` varchar(255) DEFAULT NULL, `created_at` datetime(6) NOT NULL, `updated_at` datetime(6) NOT NULL, PRIMARY KEY (`key`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; INSERT INTO `ar_internal_metadata` VALUES ('environment','development','2021-10-13 22:29:15.077180','2021-10-13 22:29:15.077180'); This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,71 +0,0 @@ -
Chris Bloom revised this gist
Oct 14, 2021 . 1 changed file with 3 additions and 3 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -110,14 +110,14 @@ def description class AdvisoryReviewTest < Minitest::Test def test_unicode_change advisory_review = AdvisoryReview.create!(advisory_payload: { "description" => "n’t" }) # Access description to ensure encoding handlers run assert_equal 3, advisory_review.reload.description.length assert_empty advisory_review.changes advisory_review.advisory_payload["description"] = "n’t" assert_predicate advisory_review, :changed? advisory_review.save! -
Chris Bloom revised this gist
Oct 14, 2021 . 1 changed file with 71 additions and 0 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,71 @@ -- $ mysqldump --no-data railstestdb -- -- -- -- MySQL dump 10.13 Distrib 8.0.26, for macos10.15 (x86_64) -- -- Host: localhost Database: railstestdb -- ------------------------------------------------------ -- Server version 5.7.35-log /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; /*!50503 SET NAMES utf8mb4 */; /*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; /*!40103 SET TIME_ZONE='+00:00' */; /*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; Warning: A partial dump from a server that has GTIDs will by default include the GTIDs of all transactions, even those that changed suppressed parts of the database. If you don't want to restore GTIDs, pass --set-gtid-purged=OFF. To make a complete dump, pass --all-databases --triggers --routines --events. SET @MYSQLDUMP_TEMP_LOG_BIN = @@SESSION.SQL_LOG_BIN; SET @@SESSION.SQL_LOG_BIN= 0; -- -- GTID state at the beginning of the backup -- SET @@GLOBAL.GTID_PURGED=/*!80000 '+'*/ '58d9a542-ba3b-11e9-ab1a-d6379339644c:1-1448871'; -- -- Table structure for table `advisory_reviews` -- DROP TABLE IF EXISTS `advisory_reviews`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `advisory_reviews` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `advisory_payload` longblob NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Table structure for table `ar_internal_metadata` -- DROP TABLE IF EXISTS `ar_internal_metadata`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `ar_internal_metadata` ( `key` varchar(255) NOT NULL, `value` varchar(255) DEFAULT NULL, `created_at` datetime(6) NOT NULL, `updated_at` datetime(6) NOT NULL, PRIMARY KEY (`key`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; SET @@SESSION.SQL_LOG_BIN = @MYSQLDUMP_TEMP_LOG_BIN; /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; -- Dump completed on 2021-10-13 21:53:51 -
Chris Bloom revised this gist
Oct 14, 2021 . 1 changed file with 2 additions and 0 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,3 +1,5 @@ # Run with `ruby rails-serialize-unicode-test.rb` require "bundler/inline" gemfile true do -
Chris Bloom created this gist
Oct 14, 2021 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,125 @@ require "bundler/inline" gemfile true do # Rails gem "rails", "~> 6.1.4.1" # Database gem "mysql2", "~> 0.5.3" end require "active_record" ActiveRecord::Base.establish_connection(adapter: "mysql2", database: "railstestdb") ActiveRecord::Base.logger = Logger.new(STDOUT) ActiveRecord::Schema.define do create_table :advisory_reviews, force: true do |t| t.longblob :advisory_payload, null: false end end require "yaml" module NormalYAML def self.load(yaml) return nil if yaml.nil? YAML.safe_load(yaml, [BigDecimal, Date, Time]) end def self.dump(data) return nil if data.nil? YAML.dump(normalize(data)) end def self.normalize(data) case data when Array [].tap do |normalized| data.each do |element| normalized << normalize(element) end end when Hash {}.tap do |normalized| data.sort.each do |key, value| unless key.is_a?(String) || key.is_a?(Symbol) || key.is_a?(Integer) raise ArgumentError, "NormalYAML requires String, Symbol, or Integer keys." end new_key = case key when Symbol then key.to_s when /\A\d+\z/ then key.to_i else key end normalized[new_key] = normalize(value) end end when TrueClass, FalseClass, NilClass, Integer, Float data when String String.new(data) when BigDecimal BigDecimal(data) when Date Date.new(data.year, data.month, data.day) when Time utc = data.utc sec_with_frac = utc.sec + utc.subsec Time.utc(utc.year, utc.month, utc.day, utc.hour, utc.min, sec_with_frac) else raise ArgumentError, "NormalYAML does not support #{data.class.name} serialization." end rescue StandardError => error Rails.logger.debug { "Failed to normalize this data:\n---\n#{data}\n---" } raise error end def self.normal?(data) normalize(data) == data rescue StandardError false end def self.valid?(yaml) begin self.load(yaml) rescue StandardError false end true end end class AdvisoryReview < ActiveRecord::Base serialize :advisory_payload, NormalYAML validates :advisory_payload, exclusion: { in: [nil] } # Forbid nil, allow {} def description advisory_payload["description"].presence end end require "minitest/autorun" class AdvisoryReviewTest < Minitest::Test def test_unicode_change advisory_review = AdvisoryReview.create!(advisory_payload: { "description" => "\u2022" }) # Access description to ensure encoding handlers run assert_equal 1, advisory_review.reload.description.length assert_empty advisory_review.changes advisory_review.advisory_payload["description"] = "\u2019" assert_predicate advisory_review, :changed? advisory_review.save! # This should have been reset after `save!` assert_empty advisory_review.changes end end