Skip to content

Commit 8165e4a

Browse files
authored
Add tests for all columns types for each DB type (#83)
- Ensure all possible columns types for each database are properly serialized in snapshots - Drop Rails 7.0 from the test suite because it doesnt properly handle `serialize :column_name, coder: JSON` even though it still works for all other things. Its getting pretty old so its simpler to just drop it from the test suite, im sure if anyone is still on Rails 7.0 its going to be super easy to upgrade to 7.1
1 parent 71d6a22 commit 8165e4a

14 files changed

Lines changed: 466 additions & 85 deletions

.github/workflows/test.yml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,6 @@ jobs:
2525
- ruby: "3.4"
2626
- ruby: "4.0"
2727
### TEST RAILS VERSIONS
28-
- ruby: "4.0"
29-
rails_version: "~> 7.0.0"
30-
db_gem_version: "~> 1.4" # fixes sqlite3 gem dependency issue
3128
- ruby: "4.0"
3229
rails_version: "~> 7.1.0"
3330
- ruby: "4.0"
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
class ModelWithAllMysqlColumnTypes < ActiveRecord::Base
2+
include ActiveSnapshot
3+
4+
self.table_name = "model_with_all_mysql_column_types"
5+
end
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
class ModelWithAllPostgresColumnTypes < ActiveRecord::Base
2+
include ActiveSnapshot
3+
4+
self.table_name = "model_with_all_postgres_column_types"
5+
end
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
class ModelWithAllSqliteColumnTypes < ActiveRecord::Base
2+
include ActiveSnapshot
3+
4+
self.table_name = "model_with_all_sqlite_column_types"
5+
end
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
class ModelWithSerializedColumns < ActiveRecord::Base
2+
include ActiveSnapshot
3+
4+
self.table_name = "model_with_serialized_columns"
5+
6+
serialize :yaml_field, coder: YAML
7+
serialize :json_field, coder: JSON
8+
end

test/dummy_app/config/application.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ class Application < Rails::Application
2424

2525
# Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
2626
# Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
27-
# config.time_zone = 'Central Time (US & Canada)'
27+
config.time_zone = "UTC"
2828

2929
# The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
3030
# config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
@@ -48,5 +48,7 @@ class Application < Rails::Application
4848
config.after_initialize do
4949
ActiveRecord::Migration.migrate(Rails.root.join("db/migrate/*").to_s)
5050
end
51+
52+
config.active_record.yaml_column_permitted_classes = [Time, Date, Symbol]
5153
end
5254
end

test/dummy_app/config/database.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
default: &default
22
<% if defined?(Mysql2) %>
33
adapter: mysql2
4-
database: active_sort_order_test
4+
database: active_snapshot_test
55

66
<% elsif defined?(PG) %>
77
adapter: postgresql
8-
database: active_sort_order_test
9-
8+
database: active_snapshot_test
9+
1010
<% elsif defined?(SQLite3) %>
1111
adapter: sqlite3
1212
database: db/test.sqlite3
Lines changed: 58 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,91 @@
11
class SetUpTestTables < ActiveRecord::Migration::Current
2-
32
def change
4-
if connection.adapter_name == "PostgreSQL"
5-
create_enum :post_enum_array, ["foo", "bar"]
6-
end
7-
83
create_table :posts do |t|
94
t.integer :a, :b
10-
115
t.integer :status, default: 0
12-
13-
if connection.adapter_name == "PostgreSQL"
14-
t.text :text_array, array: true
15-
t.integer :integer_array, array: true
16-
t.enum :enum_array, enum_type: :post_enum_array, array: true
17-
end
18-
196
t.timestamps
207
end
218

229
create_table :comments do |t|
2310
t.string :content
24-
2511
t.references :post
26-
2712
t.timestamps
2813
end
2914

3015
create_table :notes do |t|
3116
t.string :body
32-
3317
t.references :post
34-
3518
t.timestamps
3619
end
3720

3821
create_table :tasks do |t|
3922
t.string :title
40-
4123
t.references :assignee
4224
t.references :requester
43-
4425
t.timestamps
4526
end
4627

4728
create_table :users do |t|
4829
t.string :name
49-
5030
t.timestamps
5131
end
52-
end
5332

33+
create_table :model_with_serialized_columns do |t|
34+
t.text :yaml_field
35+
t.text :json_field
36+
end
37+
38+
if connection.adapter_name == "PostgreSQL"
39+
create_enum :some_enum, ["foo", "bar"]
40+
41+
create_table :model_with_all_postgres_column_types do |t|
42+
t.text :text_field
43+
t.integer :integer_field
44+
t.date :date_field
45+
t.time :time_field
46+
t.timestamp :timestamp_field
47+
t.boolean :boolean_field
48+
t.binary :binary_field
49+
t.json :json_field
50+
t.jsonb :jsonb_field
51+
t.inet :inet_field
52+
t.cidr :cidr_field
53+
t.macaddr :mac_field
54+
t.uuid :uuid_field
55+
t.bit :bit_field, limit: 8
56+
t.money :money_field
57+
t.text :text_array_field, array: true
58+
t.integer :integer_array_field, array: true
59+
t.enum :enum_array_field, enum_type: :some_enum, array: true
60+
t.enum :enum_field, enum_type: :some_enum
61+
end
62+
end
63+
64+
if connection.adapter_name == "Mysql2"
65+
create_table :model_with_all_mysql_column_types do |t|
66+
t.text :text_field
67+
t.integer :integer_field
68+
t.date :date_field
69+
t.time :time_field
70+
t.timestamp :timestamp_field
71+
t.json :json_field
72+
t.boolean :boolean_field
73+
t.binary :binary_field
74+
t.column :enum_field, "ENUM('foo', 'bar')"
75+
end
76+
end
77+
78+
if connection.adapter_name == "SQLite"
79+
create_table :model_with_all_sqlite_column_types do |t|
80+
t.text :text_field
81+
t.integer :integer_field
82+
t.date :date_field
83+
t.time :time_field
84+
t.timestamp :timestamp_field
85+
t.boolean :boolean_field
86+
t.binary :binary_field
87+
t.json :json_field
88+
end
89+
end
90+
end
5491
end

test/test_helper.rb

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#$LOAD_PATH.unshift File.expand_path("../lib", __dir__)
22
ENV["RAILS_ENV"] = "test"
3+
ENV['TZ'] = "UTC"
34

45
require "active_support/all"
56

@@ -28,6 +29,13 @@
2829
### Instantiates Rails
2930
require File.expand_path("../dummy_app/config/environment.rb", __FILE__)
3031

32+
require 'active_record/tasks/database_tasks'
33+
db_config = ActiveRecord::Base.configurations.configs_for(env_name: ENV["RAILS_ENV"]).first
34+
ActiveRecord::Tasks::DatabaseTasks.drop(db_config.configuration_hash) rescue nil
35+
ActiveRecord::Tasks::DatabaseTasks.create(db_config.configuration_hash)
36+
ActiveRecord::MigrationContext.new(File.expand_path("dummy_app/db/migrate/", __dir__)).migrate
37+
ActiveRecord::Tasks::DatabaseTasks.dump_schema(db_config, :ruby)
38+
3139
require "rails/test_help"
3240

3341
class ActiveSupport::TestCase
@@ -48,12 +56,6 @@ class ActiveSupport::TestCase
4856

4957
require "minitest/autorun"
5058

51-
# Run any available migration
52-
ActiveRecord::MigrationContext.new(File.expand_path("dummy_app/db/migrate/", __dir__)).migrate
53-
54-
# Ensure schema.rb is updated
55-
ActiveRecord::Migration.maintain_test_schema!
56-
5759
require "rspec/mocks/minitest_integration"
5860

5961
def assert_time_match(a, b)

test/unit/mysql_specific_test.rb

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
require "test_helper"
2+
3+
class MysqlSpecificTest < ActiveSupport::TestCase
4+
if defined?(Mysql2)
5+
def setup
6+
@instance = ModelWithAllMysqlColumnTypes.new(
7+
text_field: "some-text",
8+
integer_field: 2,
9+
date_field: Date.parse("2026-06-03"),
10+
time_field: (Time.parse("2026-07-04 04:05:06") if !ActiveSnapshot.config.storage_method_yaml?),
11+
timestamp_field: Time.parse("2026-08-05 01:02:03"),
12+
json_field: {string: "bar", number: 2, array: [1,2,3]},
13+
#binary_field: "0010010", # binary fields have issues currently
14+
boolean_field: true,
15+
enum_field: "bar",
16+
)
17+
18+
@instance.save!
19+
end
20+
21+
def teardown
22+
end
23+
24+
def test_restore_snapshot_handles_column_serialization
25+
snapshot = @instance.create_snapshot!
26+
27+
snapshot_item = snapshot.snapshot_items.first!
28+
29+
snapshot_item.restore_item!
30+
31+
assert_equal(
32+
{
33+
text_field: "some-text",
34+
integer_field: 2,
35+
date_field: Date.parse("2026-06-03"),
36+
json_field: {"string" => "bar", "number" => 2, "array" => [1,2,3]},
37+
binary_field: nil,
38+
boolean_field: true,
39+
enum_field: "bar",
40+
},
41+
snapshot_item.item.attributes.symbolize_keys.except(:id, :time_field, :timestamp_field)
42+
)
43+
44+
assert_time_match(Time.parse("2026-08-05 01:02:03"), snapshot_item.item.timestamp_field)
45+
46+
if !ActiveSnapshot.config.storage_method_yaml?
47+
assert_time_match(Time.parse("2000-01-01 04:05:06"), snapshot_item.item.time_field)
48+
end
49+
end
50+
51+
def test_diff_handles_column_serialization
52+
snapshot = @instance.create_snapshot!
53+
54+
@instance.assign_attributes(
55+
text_field: "other-text",
56+
integer_field: 3,
57+
date_field: Date.parse("2026-01-03"),
58+
time_field: (Time.parse("2026-07-04 07:08:09") if !ActiveSnapshot.config.storage_method_yaml?),
59+
timestamp_field: Time.parse("2026-03-05 04:05:06"),
60+
json_field: {string: "foo", number: 1, array: [4,5,6]},
61+
#binary_field: "0101010101",
62+
boolean_field: false,
63+
enum_field: "foo",
64+
)
65+
66+
@instance.save!
67+
68+
diff = ActiveSnapshot::Snapshot.diff(snapshot, @instance)
69+
70+
assert_equal(["some-text","other-text"], diff.first[:changes][:text_field])
71+
assert_equal([2,3], diff.first[:changes][:integer_field])
72+
assert_equal([Date.parse("2026-06-03"), Date.parse("2026-01-03")], diff.first[:changes][:date_field])
73+
assert_equal([{"string" => "bar", "number" => 2, "array" => [1, 2, 3]}, {"string" => "foo", "number" => 1, "array" => [4, 5, 6]}], diff.first[:changes][:json_field])
74+
assert_equal(nil, diff.first[:changes][:binary_field])
75+
assert_equal([true, false], diff.first[:changes][:boolean_field])
76+
assert_equal(["bar", "foo"], diff.first[:changes][:enum_field])
77+
78+
assert_time_match(Time.parse("2026-08-05 01:02:03"), diff.first[:changes][:timestamp_field].first)
79+
assert_time_match(Time.parse("2026-03-05 04:05:06"), diff.first[:changes][:timestamp_field].last)
80+
81+
if !ActiveSnapshot.config.storage_method_yaml?
82+
assert_time_match(Time.parse("2000-01-01 04:05:06"), diff.first[:changes][:time_field].first)
83+
assert_time_match(Time.parse("2000-01-01 07:08:09"), diff.first[:changes][:time_field].last)
84+
end
85+
end
86+
end
87+
end

0 commit comments

Comments
 (0)