aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord')
-rw-r--r--activerecord/CHANGELOG.md22
-rw-r--r--activerecord/lib/active_record/associations.rb1
-rw-r--r--activerecord/lib/active_record/associations/foreign_association.rb11
-rw-r--r--activerecord/lib/active_record/associations/has_many_association.rb9
-rw-r--r--activerecord/lib/active_record/associations/has_one_association.rb1
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb1
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb8
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb7
-rw-r--r--activerecord/lib/active_record/locking/optimistic.rb2
-rw-r--r--activerecord/lib/active_record/relation.rb2
-rw-r--r--activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb6
-rw-r--r--activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb6
-rw-r--r--activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb6
-rw-r--r--activerecord/test/cases/associations/has_many_associations_test.rb86
-rw-r--r--activerecord/test/cases/associations/has_one_associations_test.rb14
-rw-r--r--activerecord/test/cases/dirty_test.rb9
-rw-r--r--activerecord/test/models/company.rb1
17 files changed, 138 insertions, 54 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md
index 53d5ec07a3..859ba38588 100644
--- a/activerecord/CHANGELOG.md
+++ b/activerecord/CHANGELOG.md
@@ -1,3 +1,25 @@
+* Fix error message when trying to create an associated record and the foreign
+ key is missing.
+
+ Before this fix the following exception was being raised:
+
+ NoMethodError: undefined method `val' for #<Arel::Nodes::BindParam:0x007fc64d19c218>
+
+ Now the message is:
+
+ ActiveRecord::UnknownAttributeError: unknown attribute 'foreign_key' for Model.
+
+ *Rafael Mendonça França*
+
+* When a table has a composite primary key, the `primary_key` method for
+ SQLite3 and PostgreSQL adapters was only returning the first field of the key.
+ Ensures that it will return nil instead, as Active Record doesn't support
+ composite primary keys.
+
+ Fixes #18070.
+
+ *arthurnn*
+
* `validates_size_of` / `validates_length_of` do not count records,
which are `marked_for_destruction?`.
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index cd5fdd5964..14af55f327 100644
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -116,6 +116,7 @@ module ActiveRecord
autoload :Association, 'active_record/associations/association'
autoload :SingularAssociation, 'active_record/associations/singular_association'
autoload :CollectionAssociation, 'active_record/associations/collection_association'
+ autoload :ForeignAssociation, 'active_record/associations/foreign_association'
autoload :CollectionProxy, 'active_record/associations/collection_proxy'
autoload :BelongsToAssociation, 'active_record/associations/belongs_to_association'
diff --git a/activerecord/lib/active_record/associations/foreign_association.rb b/activerecord/lib/active_record/associations/foreign_association.rb
new file mode 100644
index 0000000000..fe48ecec29
--- /dev/null
+++ b/activerecord/lib/active_record/associations/foreign_association.rb
@@ -0,0 +1,11 @@
+module ActiveRecord::Associations
+ module ForeignAssociation
+ def foreign_key_present?
+ if reflection.klass.primary_key
+ owner.attribute_present?(reflection.active_record_primary_key)
+ else
+ false
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb
index 93084e0dcf..d7f655d00c 100644
--- a/activerecord/lib/active_record/associations/has_many_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_association.rb
@@ -6,6 +6,7 @@ module ActiveRecord
# If the association has a <tt>:through</tt> option further specialization
# is provided by its child HasManyThroughAssociation.
class HasManyAssociation < CollectionAssociation #:nodoc:
+ include ForeignAssociation
def handle_dependency
case options[:dependent]
@@ -153,14 +154,6 @@ module ActiveRecord
end
end
- def foreign_key_present?
- if reflection.klass.primary_key
- owner.attribute_present?(reflection.association_primary_key)
- else
- false
- end
- end
-
def concat_records(records, *)
update_counter_if_success(super, records.length)
end
diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb
index e6095d84dc..74b8c53758 100644
--- a/activerecord/lib/active_record/associations/has_one_association.rb
+++ b/activerecord/lib/active_record/associations/has_one_association.rb
@@ -2,6 +2,7 @@ module ActiveRecord
# = Active Record Belongs To Has One Association
module Associations
class HasOneAssociation < SingularAssociation #:nodoc:
+ include ForeignAssociation
def handle_dependency
case options[:dependent]
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb
index 997613d7be..6bd1b8ecae 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb
@@ -5,6 +5,7 @@ module ActiveRecord
class Bytea < Type::Binary # :nodoc:
def type_cast_from_database(value)
return if value.nil?
+ return value.to_s if value.is_a?(Type::Binary::Data)
PGconn.unescape_bytea(super)
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
index 74b0833a7e..b2a7b40ded 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
@@ -385,15 +385,15 @@ module ActiveRecord
# Returns just a table's primary key
def primary_key(table)
- row = exec_query(<<-end_sql, 'SCHEMA').rows.first
+ pks = exec_query(<<-end_sql, 'SCHEMA').rows
SELECT attr.attname
FROM pg_attribute attr
- INNER JOIN pg_constraint cons ON attr.attrelid = cons.conrelid AND attr.attnum = cons.conkey[1]
+ INNER JOIN pg_constraint cons ON attr.attrelid = cons.conrelid AND attr.attnum = any(cons.conkey)
WHERE cons.contype = 'p'
AND cons.conrelid = '#{quote_table_name(table)}'::regclass
end_sql
-
- row && row.first
+ return nil unless pks.count == 1
+ pks[0][0]
end
# Renames a table.
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
index 0f7e0fac01..67b2c6b1e6 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
@@ -418,10 +418,9 @@ module ActiveRecord
end
def primary_key(table_name) #:nodoc:
- column = table_structure(table_name).find { |field|
- field['pk'] == 1
- }
- column && column['name']
+ pks = table_structure(table_name).select { |f| f['pk'] > 0 }
+ return nil unless pks.count == 1
+ pks[0]['name']
end
def remove_index!(table_name, index_name) #:nodoc:
diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb
index 7bca38c910..9f053453bd 100644
--- a/activerecord/lib/active_record/locking/optimistic.rb
+++ b/activerecord/lib/active_record/locking/optimistic.rb
@@ -11,7 +11,7 @@ module ActiveRecord
#
# == Usage
#
- # Active Records support optimistic locking if the field +lock_version+ is present. Each update to the
+ # Active Record supports optimistic locking if the +lock_version+ field is present. Each update to the
# record increments the +lock_version+ column and the locking facilities ensure that records instantiated twice
# will let the last one saved raise a +StaleObjectError+ if the first was also updated. Example:
#
diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb
index 2f067d867c..cdafa6a50a 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
@@ -569,7 +569,7 @@ module ActiveRecord
[name, binds.fetch(name.to_s) {
case where.right
when Array then where.right.map(&:val)
- else
+ when Arel::Nodes::Casted, Arel::Nodes::Quoted
where.right.val
end
}]
diff --git a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb
index 28106d3772..85db8f4614 100644
--- a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb
+++ b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb
@@ -99,6 +99,12 @@ module ActiveRecord
end
end
+ def test_composite_primary_key
+ with_example_table '`id` INT(11), `number` INT(11), foo INT(11), PRIMARY KEY (`id`, `number`)' do
+ assert_nil @conn.primary_key('ex')
+ end
+ end
+
def test_tinyint_integer_typecasting
with_example_table '`status` TINYINT(4)' do
insert(@conn, { 'status' => 2 }, 'ex')
diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
index c3c696b871..6bb2b26cd5 100644
--- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
@@ -54,6 +54,12 @@ module ActiveRecord
end
end
+ def test_composite_primary_key
+ with_example_table 'id serial, number serial, PRIMARY KEY (id, number)' do
+ assert_nil @connection.primary_key('ex')
+ end
+ end
+
def test_primary_key_raises_error_if_table_not_found
assert_raises(ActiveRecord::StatementInvalid) do
@connection.primary_key('unobtainium')
diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
index 9d09ff49c7..029663e7f4 100644
--- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
@@ -405,6 +405,12 @@ module ActiveRecord
end
end
+ def test_composite_primary_key
+ with_example_table 'id integer, number integer, foo integer, PRIMARY KEY (id, number)' do
+ assert_nil @conn.primary_key('ex')
+ end
+ end
+
def test_supports_extensions
assert_not @conn.supports_extensions?, 'does not support extensions'
end
diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb
index d3b74aa616..21a45042fa 100644
--- a/activerecord/test/cases/associations/has_many_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_associations_test.rb
@@ -31,6 +31,8 @@ require 'models/student'
require 'models/pirate'
require 'models/ship'
require 'models/tyre'
+require 'models/subscriber'
+require 'models/subscription'
class HasManyAssociationsTestForReorderWithJoinDependency < ActiveRecord::TestCase
fixtures :authors, :posts, :comments
@@ -43,12 +45,59 @@ class HasManyAssociationsTestForReorderWithJoinDependency < ActiveRecord::TestCa
end
end
+class HasManyAssociationsTestPrimaryKeys < ActiveRecord::TestCase
+ fixtures :authors, :essays, :subscribers, :subscriptions, :people
+
+ def test_custom_primary_key_on_new_record_should_fetch_with_query
+ subscriber = Subscriber.new(nick: 'webster132')
+ assert !subscriber.subscriptions.loaded?
+
+ assert_queries 1 do
+ assert_equal 2, subscriber.subscriptions.size
+ end
+
+ assert_equal subscriber.subscriptions, Subscription.where(subscriber_id: 'webster132')
+ end
+
+ def test_association_primary_key_on_new_record_should_fetch_with_query
+ author = Author.new(:name => "David")
+ assert !author.essays.loaded?
+
+ assert_queries 1 do
+ assert_equal 1, author.essays.size
+ end
+
+ assert_equal author.essays, Essay.where(writer_id: "David")
+ end
+
+ def test_has_many_custom_primary_key
+ david = authors(:david)
+ assert_equal david.essays, Essay.where(writer_id: "David")
+ end
+
+ def test_has_many_assignment_with_custom_primary_key
+ david = people(:david)
+
+ assert_equal ["A Modest Proposal"], david.essays.map(&:name)
+ david.essays = [Essay.create!(name: "Remote Work" )]
+ assert_equal ["Remote Work"], david.essays.map(&:name)
+ end
+
+ def test_blank_custom_primary_key_on_new_record_should_not_run_queries
+ author = Author.new
+ assert !author.essays.loaded?
+
+ assert_queries 0 do
+ assert_equal 0, author.essays.size
+ end
+ end
+end
class HasManyAssociationsTest < ActiveRecord::TestCase
fixtures :accounts, :categories, :companies, :developers, :projects,
:developers_projects, :topics, :authors, :comments,
- :people, :posts, :readers, :taggings, :cars, :essays,
- :categorizations, :jobs, :tags
+ :posts, :readers, :taggings, :cars, :jobs, :tags,
+ :categorizations
def setup
Client.destroyed_client_ids.clear
@@ -1578,39 +1627,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
end
- def test_custom_primary_key_on_new_record_should_fetch_with_query
- author = Author.new(:name => "David")
- assert !author.essays.loaded?
-
- assert_queries 1 do
- assert_equal 1, author.essays.size
- end
-
- assert_equal author.essays, Essay.where(writer_id: "David")
- end
-
- def test_has_many_custom_primary_key
- david = authors(:david)
- assert_equal david.essays, Essay.where(writer_id: "David")
- end
-
- def test_has_many_assignment_with_custom_primary_key
- david = people(:david)
-
- assert_equal ["A Modest Proposal"], david.essays.map(&:name)
- david.essays = [Essay.create!(name: "Remote Work" )]
- assert_equal ["Remote Work"], david.essays.map(&:name)
- end
-
- def test_blank_custom_primary_key_on_new_record_should_not_run_queries
- author = Author.new
- assert !author.essays.loaded?
-
- assert_queries 0 do
- assert_equal 0, author.essays.size
- end
- end
-
def test_calling_first_or_last_with_integer_on_association_should_not_load_association
firm = companies(:first_firm)
firm.clients.create(:name => 'Foo')
diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb
index a69f7a5262..15b37ad709 100644
--- a/activerecord/test/cases/associations/has_one_associations_test.rb
+++ b/activerecord/test/cases/associations/has_one_associations_test.rb
@@ -273,6 +273,14 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
assert_equal account, firm.reload.account
end
+ def test_create_with_inexistent_foreign_key_failing
+ firm = Firm.create(name: 'GlobalMegaCorp')
+
+ assert_raises(ActiveRecord::UnknownAttributeError) do
+ firm.create_account_with_inexistent_foreign_key
+ end
+ end
+
def test_build
firm = Firm.new("name" => "GlobalMegaCorp")
firm.save
@@ -566,6 +574,12 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
assert_equal author.post, post
end
+ def test_has_one_loading_for_new_record
+ post = Post.create!(author_id: 42, title: 'foo', body: 'bar')
+ author = Author.new(id: 42)
+ assert author.post
+ end
+
def test_has_one_relationship_cannot_have_a_counter_cache
assert_raise(ArgumentError) do
Class.new(ActiveRecord::Base) do
diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb
index 1eaff5e293..98cf60a8c4 100644
--- a/activerecord/test/cases/dirty_test.rb
+++ b/activerecord/test/cases/dirty_test.rb
@@ -688,7 +688,14 @@ class DirtyTest < ActiveRecord::TestCase
serialize :data
end
- klass.create!(data: "foo")
+ binary = klass.create!(data: "\\\\foo")
+
+ assert_not binary.changed?
+
+ binary.data = binary.data.dup
+
+ assert_not binary.changed?
+
binary = klass.last
assert_not binary.changed?
diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb
index 42f7fb4680..5a56616eb9 100644
--- a/activerecord/test/models/company.rb
+++ b/activerecord/test/models/company.rb
@@ -72,6 +72,7 @@ class Firm < Company
# Oracle tests were failing because of that as the second fixture was selected
has_one :account_using_primary_key, -> { order('id') }, :primary_key => "firm_id", :class_name => "Account"
has_one :account_using_foreign_and_primary_keys, :foreign_key => "firm_name", :primary_key => "name", :class_name => "Account"
+ has_one :account_with_inexistent_foreign_key, class_name: 'Account', foreign_key: "inexistent"
has_one :deletable_account, :foreign_key => "firm_id", :class_name => "Account", :dependent => :delete
has_one :account_limit_500_with_hash_conditions, -> { where :credit_limit => 500 }, :foreign_key => "firm_id", :class_name => "Account"