Last active
July 10, 2023 09:57
-
-
Save sinsoku/9539b192a3165e3c89720bc79bbcb00c to your computer and use it in GitHub Desktop.
Rails create_association incompatibility
This file contains 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 characters
# 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