Created
April 17, 2016 11:43
-
-
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
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
# 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