Skip to content

Instantly share code, notes, and snippets.

@sinsoku
Last active July 10, 2023 09:57
Show Gist options
  • Save sinsoku/9539b192a3165e3c89720bc79bbcb00c to your computer and use it in GitHub Desktop.
Save sinsoku/9539b192a3165e3c89720bc79bbcb00c to your computer and use it in GitHub Desktop.
Rails create_association incompatibility
# frozen_string_literal: true
# This is a test script for create_association incompatibility.
#
# usage
# * `RAILS_VERSION=7.0.4 ruby active_record_test.rb`
# * `RAILS_VERSION=7.0.5 ruby active_record_test.rb`
#
# see also
# * https://github.com/rails/rails/issues/48330
# * https://github.com/rails/rails/issues/48632
require "bundler/inline"
gemfile(true) do
source "https://rubygems.org"
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
rails_version = ENV["RAILS_VERSION"]
if rails_version
gem "activerecord", rails_version
else
%w[activesupport activemodel activerecord].each { gem _1, github: "rails/rails", glob: "#{_1}/*.gemspec" }
end
gem "sqlite3"
end
require "active_record"
require "minitest/autorun"
require "logger"
# refs: https://github.com/rails/rails/issues/46737
# This connection will do for database-independent bug reports.
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Base.logger = Logger.new(STDOUT)
ActiveRecord::Schema.define do
create_table :users, force: true
create_table :posts, force: true do |t|
t.references :user, null: false
t.timestamps
end
create_table :uniq_posts, force: true do |t|
t.references :user, null: false, index: { unique: true }
end
end
class ApplicationRecord < ActiveRecord::Base
primary_abstract_class
end
class User < ApplicationRecord
has_one :post
has_one :post_destroy, dependent: :destroy, class_name: 'Post'
has_one :post_nullify, dependent: :nullify, class_name: 'Post'
# no validation
has_one :post_no_validation, class_name: 'PostNoValidation'
has_one :post_no_validation_destroy, dependent: :destroy, class_name: 'PostNoValidation'
has_one :post_no_validation_nullify, dependent: :nullify, class_name: 'PostNoValidation'
end
class Post < ApplicationRecord
belongs_to :user
end
class PostNoValidation < ApplicationRecord
self.table_name = 'posts'
belongs_to :user, optional: true
end
# v7.0.4 and earlier may fail to delete records.
# And it is undefined which of the duplicate records `has_one` refers to.
class PostTest < Minitest::Test
def setup
@user = User.create!
Post.create!(user: @user)
end
def teardown
[User, Post].each(&:delete_all)
end
def test_default # point
assert_raises(ActiveRecord::NotNullViolation) { @user.create_post! }
v_7_0_4? ? assert_equal(2, Post.count) : assert_equal(1, Post.count)
end
def test_destroy
@user.create_post_destroy!
assert_equal(1, Post.count)
end
def test_nullify # point
assert_raises(ActiveRecord::NotNullViolation) { @user.create_post_nullify! }
v_7_0_4? ? assert_equal(2, Post.count) : assert_equal(1, Post.count)
end
def test_no_validation # point
assert_raises(ActiveRecord::NotNullViolation) { @user.create_post_no_validation! }
v_7_0_4? ? assert_equal(2, Post.count) : assert_equal(1, Post.count)
end
def test_no_validation_destroy
@user.create_post_no_validation_destroy!
assert_equal(1, Post.count)
end
def test_no_validation_nullify # point
assert_raises(ActiveRecord::NotNullViolation) { @user.create_post_no_validation_nullify! }
v_7_0_4? ? assert_equal(2, Post.count) : assert_equal(1, Post.count)
end
private def v_7_0_4?
ENV['RAILS_VERSION'] == '7.0.4'
end
end
class User < ApplicationRecord
# uniq constraint
has_one :uniq_post
has_one :uniq_post_destroy, dependent: :destroy, class_name: 'UniqPost'
has_one :uniq_post_nullify, dependent: :nullify, class_name: 'UniqPost'
# no validation
has_one :uniq_post_no_validation, class_name: 'UniqPostNoValidation'
has_one :uniq_post_no_validation_destroy, dependent: :destroy, class_name: 'UniqPostNoValidation'
has_one :uniq_post_no_validation_nullify, dependent: :nullify, class_name: 'UniqPostNoValidation'
end
class UniqPost < ApplicationRecord
belongs_to :user
validates_uniqueness_of :user_id
end
class UniqPostNoValidation < ApplicationRecord
self.table_name = 'uniq_posts'
belongs_to :user, optional: true
end
class UniqPostTest < Minitest::Test
def setup
@user = User.create!
UniqPost.create!(user: @user)
end
def teardown
[User, UniqPost].each(&:delete_all)
end
def test_default
assert_raises(ActiveRecord::NotNullViolation) { @user.create_uniq_post! }
assert_equal(1, UniqPost.count)
end
# No exception since v7.0.5
def test_destroy
if v_7_0_4?
assert_raises(ActiveRecord::RecordInvalid) { @user.create_uniq_post_destroy! }
assert_equal(0, UniqPost.count)
else
@user.create_uniq_post_destroy!
assert_equal(1, UniqPost.count)
end
end
def test_nullify
assert_raises(ActiveRecord::NotNullViolation) { @user.create_uniq_post_nullify! }
assert_equal(1, UniqPost.count)
end
# Since v7.0.5 changes to delete existing records first, NotNullViolation exception occurs.
def test_no_validation
if v_7_0_4?
assert_raises(ActiveRecord::RecordNotUnique) { @user.create_uniq_post_no_validation! }
else
assert_raises(ActiveRecord::NotNullViolation) { @user.create_uniq_post_no_validation! }
end
assert_equal(1, UniqPost.count)
end
# No exception since v7.0.5
def test_no_validation_destroy
if v_7_0_4?
assert_raises(ActiveRecord::RecordNotUnique) { @user.create_uniq_post_no_validation_destroy! }
assert_equal(1, UniqPost.count)
else
@user.create_uniq_post_no_validation_destroy!
assert_equal(1, UniqPost.count)
end
end
# Since v7.0.5 changes to delete existing records first, NotNullViolation exception occurs.
def test_no_validation_nullify
if v_7_0_4?
assert_raises(ActiveRecord::RecordNotUnique) { @user.create_uniq_post_no_validation_nullify! }
else
assert_raises(ActiveRecord::NotNullViolation) { @user.create_uniq_post_no_validation_nullify! }
end
assert_equal(1, UniqPost.count)
end
private def v_7_0_4?
ENV['RAILS_VERSION'] == '7.0.4'
end
end
class User < ApplicationRecord
has_one :custom_post, dependent: :destroy, class_name: 'CustomPost'
end
class CustomPost < ApplicationRecord
self.table_name = 'posts'
belongs_to :user
validate :can_be_recreated_within_2_seconds
private def can_be_recreated_within_2_seconds
other = self.class.find_by(user_id:)
errors.add(:base, 'exceeded time to recreate') if other && other.created_at < 2.seconds.ago
end
end
class CustomPostTest < Minitest::Test
def setup
@user = User.create!
Post.create!(user: @user)
end
def teardown
[User, Post].each(&:delete_all)
end
def test_custom
sleep 2
if v_7_0_4?
error = assert_raises(ActiveRecord::RecordInvalid) { @user.create_custom_post! }
assert_equal 'Validation failed: exceeded time to recreate' ,error.message
else
# no raises
@user.create_custom_post!
end
end
private def v_7_0_4?
ENV['RAILS_VERSION'] == '7.0.4'
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment