Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ozydingo/f946313aec00c25f82447c70428ba138 to your computer and use it in GitHub Desktop.
Save ozydingo/f946313aec00c25f82447c70428ba138 to your computer and use it in GitHub Desktop.
Code to reproduce bug with a has_one :through referential relationship where both models have a default scope
# Foo has_one :bar, Bar belongs_to :foo
# Foo has_one parent_foo and has_many child_foos
# Bar has_one parent_foo through :foo
# Bar has_one parent_bar through :parent_foo
#
# The parent_bar association incorrectly applies values into the SQL query
# You can see this in the query generated from b1.parent_bar in the code below: notice there are 3 "?"s and 2 node values.
# SELECT "bars".* FROM "bars" INNER JOIN "foos" ON "bars"."foo_id" = "foos"."id" INNER JOIN "foo_relationships" ON "foos"."id" = "foo_relationships"."parent_foo_id" INNER JOIN "foos" "foos_parent_bar_join" ON "foo_relationships"."child_foo_id" = "foos_parent_bar_join"."id" WHERE "bars"."deleted" = ? AND "foos"."deleted" = ? AND "foos_parent_bar_join"."id" = ? LIMIT 1 [["deleted", "f"], ["id", 2]
#
# This is more noticeable when using MySQL, as it is less forgiving of missing nodes: notice that the id of '2' is being applied to `foos`.`deleted`, and final value for `foos_parent_bar_join`.`id` in the query string is simply missing
# b1.parent_bar
# Bar Load (0.6ms) SELECT `bars`.* FROM `bars` INNER JOIN `foos` ON `bars`.`foo_id` = `foos`.`id` INNER JOIN `foo_relationships` ON `foos`.`id` = `foo_relationships`.`parent_foo_id` INNER JOIN `foos` `foos_parent_bar_join` ON `foo_relationships`.`child_foo_id` = `foos_parent_bar_join`.`id` WHERE `bars`.`deleted` = 0 AND `foos`.`deleted` = 2 AND `foos_parent_bar_join`.`id` = LIMIT 1
# Mysql2::Error: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'LIMIT 1' at line 1: SELECT `bars`.* FROM `bars` INNER JOIN `foos` ON `bars`.`foo_id` = `foos`.`id` INNER JOIN `foo_relationships` ON `foos`.`id` = `foo_relationships`.`parent_foo_id` INNER JOIN `foos` `foos_parent_bar_join` ON `foo_relationships`.`child_foo_id` = `foos_parent_bar_join`.`id` WHERE `bars`.`deleted` = 0 AND `foos`.`deleted` = 2 AND `foos_parent_bar_join`.`id` = LIMIT 1
#
# There is a workaround, which is to use SQL fragments in the default_scopes instead of hashes, even for just one of the models.
# default_scope ->{where "foos.deleted = 'f'"}
# Bar.find(2).parent_bar
# Bar Load (0.3ms) SELECT `bars`.* FROM `bars` WHERE (bars.deleted = 'f') AND `bars`.`id` = 2 LIMIT 1
# Bar Load (0.4ms) SELECT `bars`.* FROM `bars` INNER JOIN `foos` ON `bars`.`foo_id` = `foos`.`id` INNER JOIN `foo_relationships` ON `foos`.`id` = `foo_relationships`.`parent_foo_id` INNER JOIN `foos` `foos_parent_bar_join` ON `foo_relationships`.`child_foo_id` = `foos_parent_bar_join`.`id` WHERE (bars.deleted = 'f') AND (foos.deleted = 'f') AND (foos.deleted = 'f') AND `foos_parent_bar_join`.`id` = 2 LIMIT 1
# => #<Bar id: 1, foo_id: "1", deleted: false>
begin
require 'bundler/inline'
rescue LoadError => e
$stderr.puts 'Bundler version 1.10 or later is required. Please update your Bundler'
raise e
end
gemfile(true) do
source 'https://rubygems.org'
# Activate the gem you are reporting the issue against.
gem 'activerecord', '4.2.3'
gem 'sqlite3'
end
require 'active_record'
require 'minitest/autorun'
require 'logger'
# Ensure backward compatibility with Minitest 4
Minitest::Test = MiniTest::Unit::TestCase unless defined?(Minitest::Test)
# 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 :foos do |t|
t.boolean :deleted, default: false
end
create_table :bars do |t|
t.string :foo_id
t.boolean :deleted, default: false
end
create_table :foo_relationships do |t|
t.integer :parent_foo_id
t.integer :child_foo_id
end
end
class Foo < ActiveRecord::Base
default_scope ->{where deleted: false}
# default_scope ->{where "foos.deleted = 'f'"}
has_one :bar
has_many :parent_to_child_relationships, class_name: "FooRelationship", foreign_key: "parent_foo_id"
has_one :child_to_parent_relationship, class_name: "FooRelationship", foreign_key: "child_foo_id"
has_many :child_foos, through: :parent_to_child_relationships, source: :child_foo
has_one :parent_foo, through: :child_to_parent_relationship, source: :parent_foo
end
class FooRelationship < ActiveRecord::Base
belongs_to :parent_foo, class_name: "Foo"
belongs_to :child_foo, class_name: "Foo"
end
class Bar < ActiveRecord::Base
default_scope ->{where deleted: false}
# default_scope ->{where "bars.deleted = 'f'"}
belongs_to :foo
has_one :parent_foo, through: :foo, source: :parent_foo
has_one :parent_bar, through: :parent_foo, source: :bar
end
class BugTest < Minitest::Test
def test_bar
f0 = Foo.create
f1 = f0.child_foos.create
b0 = f0.create_bar
b1 = f1.create_bar
assert_equal b0, f0.bar
assert_equal b1, f1.bar
assert_equal f0, f1.parent_foo
assert_equal b0, b1.parent_bar
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment