This project is archived and is in readonly mode.

#4516 ✓invalid
Adman65

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

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>

Pages