Skip to content

Instantly share code, notes, and snippets.

@composerinteralia
Last active October 13, 2021 17:57
Show Gist options
  • Save composerinteralia/40b4c703d725551e7309f83b4d033083 to your computer and use it in GitHub Desktop.
Save composerinteralia/40b4c703d725551e7309f83b4d033083 to your computer and use it in GitHub Desktop.
Bug with multiple databases, through association, and after_commit callback
# frozen_string_literal: true
require "bundler/inline"
gemfile(true) do
source "https://rubygems.org"
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
gem "rails", path: "/Users/composerinteralia/code/rails/rails"
gem "sqlite3"
end
require "active_record"
require "minitest/autorun"
require "logger"
db = Tempfile.new(['db', '.db'])
db2 = Tempfile.new(['db2', '.db'])
ActiveRecord::Base.configurations = { issues: { adapter: "sqlite3", database: db2 } }
ActiveRecord::Base.legacy_connection_handling = false
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: db)
ActiveRecord::Base.establish_connection(:issues)
ActiveRecord::Base.logger = Logger.new(STDOUT)
class Issues < ActiveRecord::Base
self.abstract_class = true
# The test passes if we comment this out so everything is in one database
connects_to database: { writing: :issues, reading: :issues }
end
ActiveRecord::Schema.define do
create_table :assignees, force: true do |t|
end
end
Issues.connection.execute('CREATE TABLE "issues" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL)')
Issues.connection.execute(<<~SQL)
CREATE TABLE "assignments" (
"id" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
"issue_id" integer,
"assignee_id" integer
)
SQL
class Issue < Issues
# The test also passes if we set `inverse_of: false` here since we'll end up with a new issue object and load the assignees on that instead
has_many :assignments
has_many :assignees, through: :assignments, disable_joins: true
end
class Assignment < Issues
belongs_to :issue
belongs_to :assignee
after_commit :load_issue_assignees
def load_issue_assignees
issue.assignees.to_a
end
end
class Assignee < ActiveRecord::Base
has_many :assignments
end
class BugTest < Minitest::Test
def test_association_stuff
issue = Issue.create!
user = Assignee.create
# When the models are in different databases, the after commit runs earlier
# (presumably because a transaction from one database won't be joinable from
# another) and so the `assignees` association gets loaded, but then we add
# this new assignee to the association target even though it's already there
# at https://github.com/rails/rails/blob/ced387ee58e9fd980217dd1f472f2c5afa241e9a/activerecord/lib/active_record/associations/collection_association.rb#L467-L470
#
# When the models are in the same database, this happens in the opposite
# order because the after_commit runs later. We add the new assignee to
# the association target, then later the after_commit runs and loads the
# whole association.
issue.assignees << user
assert_equal [user], issue.assignees.to_a
# expected: [#<Assignee id: 1>]
# actual: [#<Assignee id: 1>, #<Assignee id: 1>]
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment