This project is archived and is in readonly mode.
Correct 'type' conditions aren't added when using multiple inheritances w/STI
Reported by Adman65 | May 1st, 2010 @ 07:17 AM
I've created a repo with the code to illustrate the bug with sql logs and console logs.
Here's the link to the repo: http://github.com/Adman65/stibug
Here's the readme from the repo:
Bug Description
This application demonstrates a bug in Rail's STI using multiple levels. Here are the classes:
class Person < ActiveRecord::Base
class User < Person
Class Manager < User
class Agent < User
The bug arises when trying to find Agents or Managers through the User class. Logically, you'd expect User.find_by_name to search for Agent's and Manager's. This is not the case as I will illustrate in my console session and log file.
The following a split log from my console sesssin, annotated to help the maintainers.
Start by create super class record
Mini:stibug adam$ ./script/console
Loading development environment (Rails 2.3.5)
>> person = Person.create :name => 'Adam Hawkins'
=> #<Person id: 1, name: "Adam Hawkins", type: nil, created_at: "2010-05-01 05:41:56", updated_at: "2010-05-01 05:41:56">
Try to find the record using the other super classes. This fails as you'd expect since Adam Hawkins is a Person not a User
>> User.find_by_name 'Adam Hawkins'
=> nil
>> Agent.find_by_name 'Adam Hawkins'
=> nil
>> Person.find_by_name 'Adam Hawkins'
=> #<Person id: 1, name: "Adam Hawkins", type: nil, created_at: "2010-05-01 05:41:56", updated_at: "2010-05-01 05:41:56">
>> User.find_by_name 'Adam Hawkins'
=> nil
>> Customer.find_by_name 'Adam Hawkins'
=> nil
>> User.find_by_name 'Adam Hawkins'
=> nil
Now to demonstrate the bug. Create a new record somewhere at the bottom of the class heiarchy.
>> Agent.create :name => "Pranav Shah"
=> #<Agent id: 2, name: "Pranav Shah", type: "Agent", created_at: "2010-05-01 05:43:47", updated_at: "2010-05-01 05:43:47">
Then try to find the record through the super class
>> User.find_by_name "Pranav Shah"
=> #<Agent id: 2, name: "Pranav Shah", type: "Agent", created_at: "2010-05-01 05:43:47", updated_at: "2010-05-01 05:43:47">
At this point it works. You'll find out why in a second. Exit
the console and start over
>> exit
Mini:stibug adam$ ./script/console
Loading development environment (Rails 2.3.5)
Try to find the Agent through it's super class and it won't work
>> User.find_by_name "Pranav Shah"
=> nil
Hmm. This is curious. Let's make sure the Agent is still there:
>> Agent.find_by_name "Pranav Shah"
=> #<Agent id: 2, name: "Pranav Shah", type: "Agent", created_at: "2010-05-01 05:43:47", updated_at: "2010-05-01 05:43:47">
Ok. It's still in there. Let's try to find it through the User class again:
>> User.find_by_name "Pranav Shah"
=> #<Agent id: 2, name: "Pranav Shah", type: "Agent", created_at: "2010-05-01 05:43:47", updated_at: "2010-05-01 05:43:47">
Hmm truly curious. I wonder if it's just a problem with the Agent class. Let's try with the manager
>> Manager.create :name => "Denny Tran"
=> #<Manager id: 3, name: "Denny Tran", type: "Manager", created_at: "2010-05-01 05:46:50", updated_at: "2010-05-01 05:46:50">
>> User.find_by_name "Denny Tran"
=> #<Manager id: 3, name: "Denny Tran", type: "Manager", created_at: "2010-05-01 05:46:50", updated_at: "2010-05-01 05:46:50">
Hmm seems to be working. Exit the console and start over
>> exit
Mini:stibug adam$ ./script/console
Loading development environment (Rails 2.3.5)
>> User.find_by_name "Denny Tran"
=> nil
Well shit. Find it through the Manager class
>> Manager.find_by_name "Denny Tran"
=> #<Manager id: 3, name: "Denny Tran", type: "Manager", created_at: "2010-05-01 05:46:50", updated_at: "2010-05-01 05:46:50">
Works just fine. Let's try Finding through the User class
>> User.find_by_name "Denny Tran"
=> #<Manager id: 3, name: "Denny Tran", type: "Manager", created_at: "2010-05-01 05:46:50", updated_at: "2010-05-01 05:46:50">
Ok, so it seems you find the subclasses if the super classes have been loaded into memory (or something to that effect). Let's take a peak at the log:
User Load (0.3ms) SELECT * FROM "people" WHERE ("people"."name" = 'Adam Hawkins') LIMIT 1
SQL (0.3ms) SELECT name
FROM sqlite_master
WHERE type = 'table' AND NOT name = 'sqlite_sequence'
SQL (0.3ms) select sqlite_version(*)
SQL (3.4ms) CREATE TABLE "schema_migrations" ("version" varchar(255) NOT NULL)
SQL (1.8ms) CREATE UNIQUE INDEX "unique_schema_migrations" ON "schema_migrations" ("version")
SQL (0.3ms) SELECT name
FROM sqlite_master
WHERE type = 'table' AND NOT name = 'sqlite_sequence'
SQL (0.1ms) SELECT version FROM schema_migrations
Migrating to CreatePeople (20100501053746)
SQL (0.6ms) CREATE TABLE "people" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar(255), "type" varchar(255), "created_at" datetime, "updated_at" datetime)
SQL (0.1ms) INSERT INTO schema_migrations (version) VALUES ('20100501053746')
SQL (0.3ms) SELECT name
FROM sqlite_master
WHERE type = 'table' AND NOT name = 'sqlite_sequence'
SQL (0.2ms) SELECT version FROM schema_migrations
SQL (0.2ms) SELECT name
FROM sqlite_master
WHERE type = 'table' AND NOT name = 'sqlite_sequence'
# this is where it get's interesting
SQL (0.1ms) PRAGMA index_list("people")
Person Create (0.9ms) INSERT INTO "people" ("name", "created_at", "updated_at", "type") VALUES('Adam Hawkins', '2010-05-01 05:41:56', '2010-05-01 05:41:56', NULL)
User Load (0.2ms) SELECT * FROM "people" WHERE ("people"."name" = 'Adam Hawkins') AND ( ("people"."type" = 'User' ) ) LIMIT 1
Agent Load (0.2ms) SELECT * FROM "people" WHERE ("people"."name" = 'Adam Hawkins') AND ( ("people"."type" = 'Agent' ) ) LIMIT 1
Person Load (0.4ms) SELECT * FROM "people" WHERE ("people"."name" = 'Adam Hawkins') LIMIT 1
User Load (0.3ms) SELECT * FROM "people" WHERE ("people"."name" = 'Adam Hawkins') AND ( ("people"."type" = 'User' OR "people"."type" = 'Agent' ) ) LIMIT 1
Customer Load (0.2ms) SELECT * FROM "people" WHERE ("people"."name" = 'Adam Hawkins') AND ( ("people"."type" = 'Customer' ) ) LIMIT 1
User Load (0.3ms) SELECT * FROM "people" WHERE ("people"."name" = 'Adam Hawkins') AND ( ("people"."type" = 'User' OR "people"."type" = 'Agent' ) ) LIMIT 1
Agent Create (0.5ms) INSERT INTO "people" ("name", "created_at", "updated_at", "type") VALUES('Pranav Shah', '2010-05-01 05:43:47', '2010-05-01 05:43:47', 'Agent')
User Load (0.4ms) SELECT * FROM "people" WHERE ("people"."name" = 'Pranav Shah') AND ( ("people"."type" = 'User' OR "people"."type" = 'Agent' ) ) LIMIT 1
User Load (0.2ms) SELECT * FROM "people" WHERE ("people"."name" = 'Pranav Shah') AND ( ("people"."type" = 'User' ) ) LIMIT 1
Agent Load (0.4ms) SELECT * FROM "people" WHERE ("people"."name" = 'Pranav Shah') AND ( ("people"."type" = 'Agent' ) ) LIMIT 1
User Load (0.4ms) SELECT * FROM "people" WHERE ("people"."name" = 'Pranav Shah') AND ( ("people"."type" = 'User' OR "people"."type" = 'Agent' ) ) LIMIT 1
Manager Create (0.5ms) INSERT INTO "people" ("name", "created_at", "updated_at", "type") VALUES('Denny Tran', '2010-05-01 05:46:50', '2010-05-01 05:46:50', 'Manager')
User Load (0.5ms) SELECT * FROM "people" WHERE ("people"."name" = 'Denny Tran') AND ( ("people"."type" = 'User' OR "people"."type" = 'Agent' OR "people"."type" = 'Manager' ) ) LIMIT 1
# I inserted my comments here so you could easily see the condition errors.
# here you can see the conditions are only "user", then "manager" (as you'd expect), then User and Manager
User Load (0.2ms) SELECT * FROM "people" WHERE ("people"."name" = 'Denny Tran') AND ( ("people"."type" = 'User' ) ) LIMIT 1
Manager Load (0.4ms) SELECT * FROM "people" WHERE ("people"."name" = 'Denny Tran') AND ( ("people"."type" = 'Manager' ) ) LIMIT 1
User Load (0.4ms) SELECT * FROM "people" WHERE ("people"."name" = 'Denny Tran') AND ( ("people"."type" = 'User' OR "people"."type" = 'Manager' ) ) LIMIT 1
As you can tell by the log. The proper "people.type" conditions are not being added. But once the super classes are loaded the proper "people.type" conditions are added.
Files
You can refer to log/development.log and console.txt for uncommented log files
Seems the way around is to do an sql query through all the super classes, then the child classes work "as you'd expect"
Comments and changes to this ticket
-
Adman65 May 1st, 2010 @ 07:23 AM
- Tag changed from sti to rails 2.3.5, sti
-
Neeraj Singh May 12th, 2010 @ 04:42 AM
- Tag changed from rails 2.3.5, sti to rails 2.3.5, rails3, sti
I am able to reproduce this issue in rails edge.
As you have already figured it out, this problem will not exist in production mode.
Anyone else who might be interested in the ticket I am mentioning the sql queries that are being produced in two different cases.
SELECT "people".* FROM "people" WHERE ((("people"."type" = 'User' OR "people"."type" = 'Manager') OR "people"."type" = 'Agent')) AND ("people"."name" = 'agent') LIMIT 1 SELECT "people".* FROM "people" WHERE ("people"."type" = 'User') AND ("people"."name" = 'agent') LIMIT 1
-
Neeraj Singh May 12th, 2010 @ 12:58 PM
I have created a gist which explains the problem much more succinctly.
-
Neeraj Singh May 12th, 2010 @ 04:22 PM
This ticket can be marked as invalid. Please see http://groups.google.com/group/rubyonrails-core/browse_thread/threa... for a discussion on this topic.
-
Santiago Pastorino May 12th, 2010 @ 04:32 PM
- State changed from new to invalid
I'm going to add something to the guides to help people with this kind of issues
-
Neeraj Singh May 12th, 2010 @ 04:45 PM
@Santiago If you need any help with the doc lemme know. I can help you out. I have some time (atleast now).
Create your profile
Help contribute to this project by taking a few moments to create your personal profile. Create your profile »
<h2 style="font-size: 14px">Tickets have moved to Github</h2>
The new ticket tracker is available at <a href="https://github.com/rails/rails/issues">https://github.com/rails/rails/issues</a>