diff options
Diffstat (limited to 'activerecord')
24 files changed, 203 insertions, 64 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index a4de332d4c..2140b1ea83 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,18 @@ +* Bring back `db:test:prepare` to synchronize the test database schema. + + Manual synchronization using `bin/rake db:test:prepare` is required + when a migration is rolled-back, edited and reapplied. + + `ActiveRecord::Base.maintain_test_schema` now uses `db:test:prepare` + to synchronize the schema. Plugins can use this task as a hook to + provide custom behavior after the schema has been loaded. + + NOTE: `test:prepare` runs before the schema was synchronized. + + Fixes #17171, #15787. + + *Yves Senn* + * Change `reflections` public api to return the keys as String objects. Fixes #16928. @@ -14,6 +29,12 @@ *Yves Senn* +* Fix includes on association with a scope containing joins along with conditions + on the joined assoiciation. + + *Siddharth Sharma* + + * Add `Table#name` to match `TableDefinition#name`. *Cody Cutrer* diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec index bc10e96244..834fffeb18 100644 --- a/activerecord/activerecord.gemspec +++ b/activerecord/activerecord.gemspec @@ -24,5 +24,5 @@ Gem::Specification.new do |s| s.add_dependency 'activesupport', version s.add_dependency 'activemodel', version - s.add_dependency 'arel', '>= 6.0.0.beta2', '< 6.1' + s.add_dependency 'arel', '~> 6.0' end diff --git a/activerecord/lib/active_record/associations/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb index a6a1947148..0c3234ed24 100644 --- a/activerecord/lib/active_record/associations/alias_tracker.rb +++ b/activerecord/lib/active_record/associations/alias_tracker.rb @@ -57,20 +57,10 @@ module ActiveRecord end def aliased_table_for(table_name, aliased_name) - table_alias = aliased_name_for(table_name, aliased_name) - - if table_alias == table_name - Arel::Table.new(table_name) - else - Arel::Table.new(table_name).alias(table_alias) - end - end - - def aliased_name_for(table_name, aliased_name) if aliases[table_name].zero? # If it's zero, we can have our table_name aliases[table_name] = 1 - table_name + Arel::Table.new(table_name) else # Otherwise, we need to use an alias aliased_name = connection.table_alias_for(aliased_name) @@ -78,11 +68,12 @@ module ActiveRecord # Update the count aliases[aliased_name] += 1 - if aliases[aliased_name] > 1 + table_alias = if aliases[aliased_name] > 1 "#{truncate(aliased_name)}_#{aliases[aliased_name]}" else aliased_name end + Arel::Table.new(table_name).alias(table_alias) end end diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index d59cebb08b..2b7d39893d 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -358,6 +358,7 @@ module ActiveRecord if owner.new_record? replace_records(other_array, original_target) else + replace_common_records_in_memory(other_array, original_target) if other_array != original_target transaction { replace_records(other_array, original_target) } end @@ -385,11 +386,18 @@ module ActiveRecord target end - def add_to_target(record, skip_callbacks = false) + def add_to_target(record, skip_callbacks = false, &block) + if association_scope.distinct_value + index = @target.index(record) + end + replace_on_target(record, index, skip_callbacks, &block) + end + + def replace_on_target(record, index, skip_callbacks) callback(:before_add, record) unless skip_callbacks yield(record) if block_given? - if association_scope.distinct_value && index = @target.index(record) + if index @target[index] = record else @target << record @@ -534,6 +542,14 @@ module ActiveRecord target end + def replace_common_records_in_memory(new_target, original_target) + common_records = new_target & original_target + common_records.each do |record| + skip_callbacks = true + replace_on_target(record, @target.index(record), skip_callbacks) + end + end + def concat_records(records, should_raise = false) result = true diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index c5c4edd090..285d0ec9af 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -94,7 +94,7 @@ module ActiveRecord # def initialize(base, associations, joins) @alias_tracker = AliasTracker.create(base.connection, joins) - @alias_tracker.aliased_name_for(base.table_name, base.table_name) # Updates the count for base.table_name to 1 + @alias_tracker.aliased_table_for(base.table_name, base.table_name) # Updates the count for base.table_name to 1 tree = self.class.make_tree associations @join_root = JoinBase.new base, build(tree, base) @join_root.children.each { |child| construct_tables! @join_root, child } diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index 496c426986..7d6523dbc4 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -142,14 +142,8 @@ module ActiveRecord scope._select! preload_values[:select] || values[:select] || table[Arel.star] scope.includes! preload_values[:includes] || values[:includes] - - if preload_values.key? :order - scope.order! preload_values[:order] - else - if values.key? :order - scope.order! values[:order] - end - end + scope.joins! preload_values[:joins] || values[:joins] + scope.order! preload_values[:order] || values[:order] if preload_values[:readonly] || values[:readonly] scope.readonly! diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index 167453657d..c824a7b11b 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -285,7 +285,9 @@ module ActiveRecord end end + #-- # DATABASE STATEMENTS ====================================== + #++ def clear_cache! super diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index 5b83131f0e..d19bd80576 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -66,7 +66,9 @@ module ActiveRecord exception.error_number if exception.respond_to?(:error_number) end + #-- # QUOTING ================================================== + #++ def quote_string(string) @connection.escape(string) @@ -80,7 +82,9 @@ module ActiveRecord end end + #-- # CONNECTION MANAGEMENT ==================================== + #++ def active? return false unless @connection @@ -104,7 +108,9 @@ module ActiveRecord end end + #-- # DATABASE STATEMENTS ====================================== + #++ def explain(arel, binds = []) sql = "EXPLAIN #{to_sql(arel, binds.dup)}" diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index 8b7459ef27..53ad71b445 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -137,7 +137,9 @@ module ActiveRecord @connection.quote(string) end + #-- # CONNECTION MANAGEMENT ==================================== + #++ def active? if @connection.respond_to?(:stat) @@ -178,7 +180,9 @@ module ActiveRecord end end + #-- # DATABASE STATEMENTS ====================================== + #++ def select_rows(sql, name = nil, binds = []) @connection.query_with_result = true diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb index f95f45c689..991c41327f 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -14,22 +14,6 @@ module ActiveRecord @connection.unescape_bytea(value) if value end - # Quotes PostgreSQL-specific data types for SQL input. - def quote(value, column = nil) #:nodoc: - return super unless column - - case value - when Float - if value.infinite? || value.nan? - "'#{value}'" - else - super - end - else - super - end - end - # Quotes strings for use in SQL input. def quote_string(s) #:nodoc: @connection.escape(s) @@ -94,6 +78,12 @@ module ActiveRecord elsif value.hex? "X'#{value}'" end + when Float + if value.infinite? || value.nan? + "'#{value}'" + else + super + end else super 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 515ff2dd44..193c950261 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -433,7 +433,12 @@ module ActiveRecord quoted_table_name = quote_table_name(table_name) sql_type = type_to_sql(type, options[:limit], options[:precision], options[:scale]) sql_type << "[]" if options[:array] - execute "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{sql_type}" + sql = "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{sql_type}" + sql << " USING #{options[:using]}" if options[:using] + if options[:cast_as] + sql << " USING CAST(#{quote_column_name(column_name)} AS #{type_to_sql(options[:cast_as], options[:limit], options[:precision], options[:scale])})" + end + execute sql change_column_default(table_name, column_name, options[:default]) if options_include_default?(options) change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null) diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index b18cb353f1..0b8b6a2bf8 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -266,7 +266,9 @@ module ActiveRecord end end + #-- # DATABASE STATEMENTS ====================================== + #++ def explain(arel, binds = []) sql = "EXPLAIN QUERY PLAN #{to_sql(arel, binds)}" diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 2024b225e4..92f2951f2d 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -184,8 +184,8 @@ module ActiveRecord # you wish to downgrade. Alternatively, you can also use the STEP option if you # wish to rollback last few migrations. <tt>rake db:migrate STEP=2</tt> will rollback # the latest two migrations. - # - # If any of the migrations throw an <tt>ActiveRecord::IrreversibleMigration</tt> exception, + # + # If any of the migrations throw an <tt>ActiveRecord::IrreversibleMigration</tt> exception, # that step will fail and you'll have some manual work to do. # # == Database support @@ -395,7 +395,14 @@ module ActiveRecord def load_schema_if_pending! if ActiveRecord::Migrator.needs_migration? || !ActiveRecord::Migrator.any_migrations? - ActiveRecord::Tasks::DatabaseTasks.load_schema_current_if_exists + # Roundrip to Rake to allow plugins to hook into database initialization. + FileUtils.cd Rails.root do + current_config = Base.connection_config + Base.clear_all_connections! + system("bin/rake db:test:prepare") + # Establish a new connection, the old database may be gone (db:test:prepare uses purge) + Base.establish_connection(current_config) + end check_pending! end end diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index a4ceacbf44..f1bdbc845c 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -36,8 +36,6 @@ module ActiveRecord config.eager_load_namespaces << ActiveRecord rake_tasks do - require "active_record/base" - namespace :db do task :load_config do ActiveRecord::Tasks::DatabaseTasks.database_configuration = Rails.application.config.database_configuration diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 3ec25f9f17..21b1c3f721 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -305,7 +305,7 @@ db_namespace = namespace :db do end # desc "Recreate the test database from the current schema" - task :load => %w(db:test:deprecated db:test:purge) do + task :load => %w(db:test:purge) do case ActiveRecord::Base.schema_format when :ruby db_namespace["test:load_schema"].invoke @@ -315,7 +315,7 @@ db_namespace = namespace :db do end # desc "Recreate the test database from an existent schema.rb file" - task :load_schema => %w(db:test:deprecated db:test:purge) do + task :load_schema => %w(db:test:purge) do begin should_reconnect = ActiveRecord::Base.connection_pool.active_connection? ActiveRecord::Schema.verbose = false @@ -328,7 +328,7 @@ db_namespace = namespace :db do end # desc "Recreate the test database from an existent structure.sql file" - task :load_structure => %w(db:test:deprecated db:test:purge) do + task :load_structure => %w(db:test:purge) do ActiveRecord::Tasks::DatabaseTasks.load_schema_for ActiveRecord::Base.configurations['test'], :sql, ENV['SCHEMA'] end @@ -349,12 +349,12 @@ db_namespace = namespace :db do task :clone_structure => %w(db:test:deprecated db:structure:dump db:test:load_structure) # desc "Empty the test database" - task :purge => %w(db:test:deprecated environment load_config) do + task :purge => %w(environment load_config) do ActiveRecord::Tasks::DatabaseTasks.purge ActiveRecord::Base.configurations['test'] end # desc 'Check for pending migrations and load the test schema' - task :prepare => %w(db:test:deprecated environment load_config) do + task :prepare => %w(environment load_config) do unless ActiveRecord::Base.configurations.blank? db_namespace['test:load'].invoke end diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 460daf99bc..561ed222d1 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -304,11 +304,11 @@ module ActiveRecord klass.current_scope = previous end - # Updates all records with details given if they match a set of conditions supplied, limits and order can - # also be supplied. This method constructs a single SQL UPDATE statement and sends it straight to the - # database. It does not instantiate the involved models and it does not trigger Active Record callbacks - # or validations. Values passed to `update_all` will not go through ActiveRecord's type-casting behavior. - # It should receive only values that can be passed as-is to the SQL database. + # Updates all records in the current relation with details given. This method constructs a single SQL UPDATE + # statement and sends it straight to the database. It does not instantiate the involved models and it does not + # trigger Active Record callbacks or validations. Values passed to `update_all` will not go through + # ActiveRecord's type-casting behavior. It should receive only values that can be passed as-is to the SQL + # database. # # ==== Parameters # diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index eb69943551..6e384facce 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -427,14 +427,12 @@ module ActiveRecord # => SELECT "users".* FROM "users" LEFT JOIN bookmarks ON bookmarks.bookmarkable_type = 'Post' AND bookmarks.user_id = users.id def joins(*args) check_if_method_has_arguments!(:joins, args) - - args.compact! - args.flatten! - spawn.joins!(*args) end def joins!(*args) # :nodoc: + args.compact! + args.flatten! self.joins_values += args self end diff --git a/activerecord/lib/active_record/type/integer.rb b/activerecord/lib/active_record/type/integer.rb index 36bbd9cd5e..750f353472 100644 --- a/activerecord/lib/active_record/type/integer.rb +++ b/activerecord/lib/active_record/type/integer.rb @@ -38,7 +38,7 @@ module ActiveRecord def ensure_in_range(value) unless range.cover?(value) - raise RangeError, "#{value} is too large for #{self.class} with limit #{limit || 4}" + raise RangeError, "#{value} is out of range for #{self.class} with limit #{limit || 4}" end end diff --git a/activerecord/lib/active_record/type/type_map.rb b/activerecord/lib/active_record/type/type_map.rb index 7c194c0cdf..09f5ba6b74 100644 --- a/activerecord/lib/active_record/type/type_map.rb +++ b/activerecord/lib/active_record/type/type_map.rb @@ -1,10 +1,12 @@ +require 'thread_safe' + module ActiveRecord module Type class TypeMap # :nodoc: def initialize @mapping = {} - @cache = Hash.new do |h, key| - h[key] = {} + @cache = ThreadSafe::Cache.new do |h, key| + h.fetch_or_store(key, ThreadSafe::Cache.new) end end @@ -13,7 +15,9 @@ module ActiveRecord end def fetch(lookup_key, *args, &block) - @cache[lookup_key][args] ||= perform_fetch(lookup_key, *args, &block) + @cache[lookup_key].fetch_or_store(args) do + perform_fetch(lookup_key, *args, &block) + end end def register_type(key, value = nil, &block) diff --git a/activerecord/test/cases/adapters/postgresql/change_schema_test.rb b/activerecord/test/cases/adapters/postgresql/change_schema_test.rb new file mode 100644 index 0000000000..ec1b446dab --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/change_schema_test.rb @@ -0,0 +1,26 @@ +require 'cases/helper' + +module ActiveRecord + class Migration + class PGChangeSchemaTest < ActiveRecord::TestCase + attr_reader :connection + + def setup + super + @connection = ActiveRecord::Base.connection + connection.create_table(:strings) do |t| + t.string :somedate + end + end + + def teardown + connection.drop_table :strings + end + + def test_change_string_to_date + connection.change_column :strings, :somedate, :timestamp, using: 'CAST("somedate" AS timestamp)' + assert_equal :datetime, connection.columns(:strings).find { |c| c.name == 'somedate' }.type + end + end + end +end diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index 4539b99504..dd4f530791 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -904,6 +904,12 @@ class EagerAssociationTest < ActiveRecord::TestCase assert_no_queries {assert_equal posts(:sti_comments), comment.post} end + def test_eager_association_with_scope_with_joins + assert_nothing_raised do + Post.includes(:very_special_comment_with_post_with_joins).to_a + end + end + def test_preconfigured_includes_with_has_many posts = authors(:david).posts_with_comments one = posts.detect { |p| p.id == 1 } diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 5a963b6458..8b7ab11570 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -1997,4 +1997,68 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal 1, car.bulbs.count assert_equal 1, car.tyres.count end + + test 'associations replace in memory when records have the same id' do + bulb = Bulb.create! + car = Car.create!(bulbs: [bulb]) + + new_bulb = Bulb.find(bulb.id) + new_bulb.name = "foo" + car.bulbs = [new_bulb] + + assert_equal "foo", car.bulbs.first.name + end + + test 'in memory replacement executes no queries' do + bulb = Bulb.create! + car = Car.create!(bulbs: [bulb]) + + new_bulb = Bulb.find(bulb.id) + + assert_no_queries do + car.bulbs = [new_bulb] + end + end + + test 'in memory replacements do not execute callbacks' do + raise_after_add = false + klass = Class.new(ActiveRecord::Base) do + self.table_name = :cars + has_many :bulbs, after_add: proc { raise if raise_after_add } + + def self.name + "Car" + end + end + bulb = Bulb.create! + car = klass.create!(bulbs: [bulb]) + + new_bulb = Bulb.find(bulb.id) + raise_after_add = true + + assert_nothing_raised do + car.bulbs = [new_bulb] + end + end + + test 'in memory replacements sets inverse instance' do + bulb = Bulb.create! + car = Car.create!(bulbs: [bulb]) + + new_bulb = Bulb.find(bulb.id) + car.bulbs = [new_bulb] + + assert_same car, new_bulb.car + end + + test 'in memory replacement maintains order' do + first_bulb = Bulb.create! + second_bulb = Bulb.create! + car = Car.create!(bulbs: [first_bulb, second_bulb]) + + same_bulb = Bulb.find(first_bulb.id) + car.bulbs = [second_bulb, same_bulb] + + assert_equal [first_bulb, second_bulb], car.bulbs + end end diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index 84abaf0291..094fcccc89 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -209,6 +209,10 @@ class ReflectionTest < ActiveRecord::TestCase assert_not_equal Object.new, Firm._reflections['clients'] end + def test_reflections_should_return_keys_as_strings + assert Category.reflections.keys.all? { |key| key.is_a? String }, "Model.reflections is expected to return string for keys" + end + def test_has_and_belongs_to_many_reflection assert_equal :has_and_belongs_to_many, Category.reflections['posts'].macro assert_equal :posts, Category.reflect_on_all_associations(:has_and_belongs_to_many).first.name diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index 36cf221d45..a9996e5236 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -78,6 +78,7 @@ class Post < ActiveRecord::Base has_one :very_special_comment has_one :very_special_comment_with_post, -> { includes(:post) }, :class_name => "VerySpecialComment" + has_one :very_special_comment_with_post_with_joins, -> { joins(:post).order('posts.id') }, class_name: "VerySpecialComment" has_many :special_comments has_many :nonexistant_comments, -> { where 'comments.id < 0' }, :class_name => 'Comment' |