Permalink
Browse files

Raise error on unknown primary key.

If we don't have a primary key when we ask for it, it's better to fail
fast. Fixes GH #2307.
  • Loading branch information...
1 parent 5711a35 commit ee2be435b1e5c0e94a4ee93a1a310e0471a77d07 @jonleighton jonleighton committed Oct 5, 2011
View
7 activerecord/lib/active_record/attribute_methods/primary_key.rb
@@ -14,6 +14,8 @@ module ClassMethods
# primary_key_prefix_type setting, though.
def primary_key
@primary_key ||= reset_primary_key
+ raise ActiveRecord::UnknownPrimaryKey.new(self) unless @primary_key
+ @primary_key
end
# Returns a quoted version of the primary key name, used to construct SQL statements.
@@ -29,6 +31,11 @@ def reset_primary_key #:nodoc:
key
end
+ def primary_key? #:nodoc:
+ @primary_key ||= reset_primary_key
+ !@primary_key.nil?
+ end
+
def get_primary_key(base_name) #:nodoc:
return 'id' unless base_name && !base_name.blank?
View
6 activerecord/lib/active_record/attribute_methods/read.rb
@@ -40,7 +40,7 @@ def define_method_attribute(attr_name)
define_read_method(attr_name, attr_name, columns_hash[attr_name])
end
- if attr_name == primary_key && attr_name != "id"
+ if primary_key? && attr_name == primary_key && attr_name != "id"
define_read_method('id', attr_name, columns_hash[attr_name])
end
end
@@ -63,7 +63,7 @@ def define_read_method(method_name, attr_name, column)
cast_code = column.type_cast_code('v')
access_code = "(v=@attributes['#{attr_name}']) && #{cast_code}"
- unless attr_name.to_s == self.primary_key.to_s
+ unless primary_key? && attr_name.to_s == primary_key.to_s
access_code.insert(0, "missing_attribute('#{attr_name}', caller) unless @attributes.has_key?('#{attr_name}'); ")
end
@@ -107,7 +107,7 @@ def read_attribute(attr_name)
def _read_attribute(attr_name)
attr_name = attr_name.to_s
- attr_name = self.class.primary_key if attr_name == 'id'
+ attr_name = self.class.primary_key? && self.class.primary_key if attr_name == 'id'
value = @attributes[attr_name]
unless value.nil?
if column = column_for_attribute(attr_name)
View
2 activerecord/lib/active_record/attribute_methods/write.rb
@@ -18,7 +18,7 @@ def define_method_attribute=(attr_name)
end
end
- if attr_name == primary_key && attr_name != "id"
+ if primary_key? && attr_name == primary_key && attr_name != "id"
generated_attribute_methods.module_eval("alias :id= :'#{primary_key}='")
end
end
View
9 activerecord/lib/active_record/base.rb
@@ -708,7 +708,7 @@ def table_exists?
# Returns an array of column objects for the table associated with this class.
def columns
if defined?(@primary_key)
- connection_pool.primary_keys[table_name] ||= primary_key
+ connection_pool.primary_keys[table_name] ||= @primary_key
end
connection_pool.columns[table_name]
@@ -953,7 +953,7 @@ def before_remove_const #:nodoc:
# objects of different types from the same table.
def instantiate(record)
sti_class = find_sti_class(record[inheritance_column])
- record_id = sti_class.primary_key && record[sti_class.primary_key]
+ record_id = sti_class.primary_key? && record[sti_class.primary_key]
if ActiveRecord::IdentityMap.enabled? && record_id
if (column = sti_class.columns_hash[sti_class.primary_key]) && column.number?
@@ -1941,8 +1941,9 @@ def ensure_proper_type
# The primary key and inheritance column can never be set by mass-assignment for security reasons.
def self.attributes_protected_by_default
- default = [ primary_key, inheritance_column ]
- default << 'id' unless primary_key.eql? 'id'
+ default = [ inheritance_column ]
+ default << primary_key if primary_key?
+ default << 'id' unless primary_key? && primary_key == 'id'
default
end
View
14 activerecord/lib/active_record/errors.rb
@@ -169,4 +169,18 @@ def initialize(errors)
@errors = errors
end
end
+
+ # Raised when a model attempts to fetch its primary key from the database, but the table
+ # has no primary key declared.
+ class UnknownPrimaryKey < ActiveRecordError
+ attr_reader :model
+
+ def initialize(model)
+ @model = model
+ end
+
+ def message
+ "Unknown primary key for table #{model.table_name} in model #{model}."
+ end
+ end
end
View
2 activerecord/lib/active_record/fixtures.rb
@@ -622,7 +622,7 @@ def table_rows
private
def primary_key_name
- @primary_key_name ||= model_class && model_class.primary_key
+ @primary_key_name ||= model_class && model_class.primary_key? && model_class.primary_key
end
def has_primary_key_column?
View
2 activerecord/lib/active_record/persistence.rb
@@ -314,7 +314,7 @@ def create
new_id = self.class.unscoped.insert attributes_values
- self.id ||= new_id if self.class.primary_key
+ self.id ||= new_id if self.class.primary_key?
IdentityMap.add(self) if IdentityMap.enabled?
@new_record = false
View
6 activerecord/lib/active_record/relation.rb
@@ -13,7 +13,7 @@ class Relation
# These are explicitly delegated to improve performance (avoids method_missing)
delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to => :to_a
- delegate :table_name, :quoted_table_name, :primary_key, :quoted_primary_key, :connection, :column_hash,:to => :klass
+ delegate :table_name, :quoted_table_name, :primary_key, :primary_key?, :quoted_primary_key, :connection, :column_hash,:to => :klass
attr_reader :table, :klass, :loaded
attr_accessor :extensions, :default_scoped
@@ -36,7 +36,7 @@ def initialize(klass, table)
def insert(values)
primary_key_value = nil
- if primary_key && Hash === values
+ if primary_key? && Hash === values
primary_key_value = values[values.keys.find { |k|
k.name == primary_key
}]
@@ -70,7 +70,7 @@ def insert(values)
conn.insert(
im,
'SQL',
- primary_key,
+ primary_key? && primary_key,
primary_key_value,
nil,
binds)
View
2 activerecord/lib/active_record/transactions.rb
@@ -303,7 +303,7 @@ def with_transaction_returning_status
# Save the new record state and id of a record so it can be restored later if a transaction fails.
def remember_transaction_record_state #:nodoc
@_start_transaction_state ||= {}
- @_start_transaction_state[:id] = id if has_attribute?(self.class.primary_key)
+ @_start_transaction_state[:id] = id if self.class.primary_key?
unless @_start_transaction_state.include?(:new_record)
@_start_transaction_state[:new_record] = @new_record
end
View
4 activerecord/test/cases/attribute_methods/read_test.rb
@@ -24,6 +24,10 @@ def self.column_names
def self.primary_key
end
+ def self.primary_key?
+ false
+ end
+
def self.columns
column_names.map { FakeColumn.new(name) }
end
View
4 activerecord/test/cases/base_test.rb
@@ -97,10 +97,6 @@ def test_columns_should_obey_set_primary_key
assert pk.primary, 'nick should be primary key'
end
- def test_primary_key_with_no_id
- assert_nil Edge.primary_key
- end
-
unless current_adapter?(:PostgreSQLAdapter,:OracleAdapter,:SQLServerAdapter)
def test_limit_with_comma
assert_nothing_raised do
View
14 activerecord/test/cases/primary_keys_test.rb
@@ -5,6 +5,7 @@
require 'models/movie'
require 'models/keyboard'
require 'models/mixed_case_monkey'
+require 'models/edge'
class PrimaryKeysTest < ActiveRecord::TestCase
fixtures :topics, :subscribers, :movies, :mixed_case_monkeys
@@ -161,4 +162,17 @@ def test_set_primary_key_with_no_connection
assert_equal 'foo', model.primary_key
end
+
+ def test_no_primary_key_raises
+ assert_raises(ActiveRecord::UnknownPrimaryKey) do
+ Edge.primary_key
+ end
+
+ begin
+ Edge.primary_key
+ rescue ActiveRecord::UnknownPrimaryKey => e
+ assert e.message.include?('edges')
+ assert e.message.include?('Edge')
+ end
+ end
end

9 comments on commit ee2be43

@jonleighton
Ruby on Rails member

@spastorino @tenderlove @josevalim I haven't cherry-picked this into 3-1-stable. Please could you review and check that you're okay with it? Or else we need to figure out another way of dealing with #2307 (perhaps ignoring for now).

We're now doing primary_key? && primary_key in a few places. That's not ideal but can be cleaned up later I think.

@jonleighton
Ruby on Rails member

Oops, I meant to link #3207.

@spastorino
Ruby on Rails member

My vote is to not merge this to 3-1-stable, just use set_primary_key or add a primary key to the schema

@tenderlove
Ruby on Rails member

Is it really necessary to raise the exception here? From our discussion, it seemed better if we raise the exception from the code that is doing the join. Something like raise CannotJoin unless blah.primary_key?. Raising here seems very heavy handed, especially since we already return nil to indicate that there is no primary_key (which is the behavior I would expect).

@tenderlove
Ruby on Rails member

@spastorino my vote is to revert this, and raise an exception where the actual error is occurring (during join construction). ;-)

@jonleighton
Ruby on Rails member

I agree it feels heavy handed.

The thing is, there are multiple places where we're calling primary_key and expect it to be non-nil. (E.g. search for primary_key in reflection.rb.) Raising every time at source would get painful.

Maybe we should revert this and implement a primary_key! method that raises on nil? Then, change all the calls to primary_key in Reflection to be primary_key!. That should solve the majority of cases, although I am sure there will still be edge cases that are missed.

What do you think?

@yahonda

I prefer this commit revoked or implemented in different way.
because rake test_oracle got 141 errors with Oracle enhanced adapter with following reasons.

ActiveRecord::UnknownPrimaryKey: Unknown primary key for table edges in model Edge.
ActiveRecord::UnknownPrimaryKey: Unknown primary key for table mateys in model Matey.

if it's better to file a new issue, I'll do that.

@jonleighton
Ruby on Rails member

Reverted here 6474765

New fix here 2e9e647

@yahonda

Thanks! new fix works like a charm.

Please sign in to comment.