diff options
Diffstat (limited to 'activerecord/lib/active_record')
19 files changed, 121 insertions, 59 deletions
diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index cf22b850b9..705a5571ee 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -56,6 +56,10 @@ module ActiveRecord @inversed = false end + def reset_negative_cache # :nodoc: + reset if loaded? && target.nil? + end + # Reloads the \target and returns +self+ on success. # The QueryCache is cleared if +force+ is true. def reload(force = false) diff --git a/activerecord/lib/active_record/associations/builder/has_one.rb b/activerecord/lib/active_record/associations/builder/has_one.rb index 27ebe8cb71..db8393930e 100644 --- a/activerecord/lib/active_record/associations/builder/has_one.rb +++ b/activerecord/lib/active_record/associations/builder/has_one.rb @@ -32,15 +32,12 @@ module ActiveRecord::Associations::Builder # :nodoc: end end - def self.touch_record(o, name, touch) - record = o.send name + def self.touch_record(record, name, touch) + instance = record.send(name) - return unless record && record.persisted? - - if touch != true - record.touch(touch) - else - record.touch + if instance&.persisted? + touch != true ? + instance.touch(touch) : instance.touch end end @@ -48,11 +45,9 @@ module ActiveRecord::Associations::Builder # :nodoc: name = reflection.name touch = reflection.options[:touch] - callback = lambda { |record| - HasOne.touch_record(record, name, touch) - } - + callback = -> (record) { HasOne.touch_record(record, name, touch) } model.after_create callback, if: :saved_changes? + model.after_create_commit { association(name).reset_negative_cache } model.after_update callback, if: :saved_changes? model.after_destroy callback model.after_touch callback diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index 0db0ad8595..404a7c02ba 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -100,7 +100,7 @@ module ActiveRecord # converting them into an array and iterating through them using # Array#select. # - # person.pets.select { |pet| pet.name =~ /oo/ } + # person.pets.select { |pet| /oo/.match?(pet.name) } # # => [ # # #<Pet id: 2, name: "Spook", person_id: 1>, # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> diff --git a/activerecord/lib/active_record/attribute_methods/query.rb b/activerecord/lib/active_record/attribute_methods/query.rb index 0cf67644af..5a21e36cc7 100644 --- a/activerecord/lib/active_record/attribute_methods/query.rb +++ b/activerecord/lib/active_record/attribute_methods/query.rb @@ -17,7 +17,7 @@ module ActiveRecord when false, nil then false else if !type_for_attribute(attr_name) { false } - if Numeric === value || value !~ /[^0-9]/ + if Numeric === value || !value.match?(/[^0-9]/) !value.to_i.zero? else return false if ActiveModel::Type::Boolean::FALSE_VALUES.include?(value) 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 ef1eef6b69..0fe16270ed 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -572,7 +572,8 @@ module ActiveRecord end end - # See https://dev.mysql.com/doc/refman/5.7/en/error-messages-server.html + # See https://dev.mysql.com/doc/refman/5.7/en/server-error-reference.html + ER_DB_CREATE_EXISTS = 1007 ER_DUP_ENTRY = 1062 ER_NOT_NULL_VIOLATION = 1048 ER_NO_REFERENCED_ROW = 1216 @@ -592,6 +593,8 @@ module ActiveRecord def translate_exception(exception, message:, sql:, binds:) case error_number(exception) + when ER_DB_CREATE_EXISTS + DatabaseAlreadyExists.new(message, sql: sql, binds: binds) when ER_DUP_ENTRY RecordNotUnique.new(message, sql: sql, binds: binds) when ER_NO_REFERENCED_ROW, ER_ROW_IS_REFERENCED, ER_ROW_IS_REFERENCED_2, ER_NO_REFERENCED_ROW_2 diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb index 0732b69f81..20041f0c85 100644 --- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb @@ -275,7 +275,7 @@ module ActiveRecord # hash and merges with the rest of the hash. # Connection details inside of the "url" key win any merge conflicts def resolve_hash_connection(spec) - if spec["url"] && spec["url"] !~ /^jdbc:/ + if spec["url"] && !spec["url"].match?(/^jdbc:/) connection_hash = resolve_url_connection(spec.delete("url")) spec.merge!(connection_hash) end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 0a7c6d8ac4..f33ba87435 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -467,6 +467,7 @@ module ActiveRecord UNIQUE_VIOLATION = "23505" SERIALIZATION_FAILURE = "40001" DEADLOCK_DETECTED = "40P01" + DUPLICATE_DATABASE = "42P04" LOCK_NOT_AVAILABLE = "55P03" QUERY_CANCELED = "57014" @@ -488,6 +489,8 @@ module ActiveRecord SerializationFailure.new(message, sql: sql, binds: binds) when DEADLOCK_DETECTED Deadlocked.new(message, sql: sql, binds: binds) + when DUPLICATE_DATABASE + DatabaseAlreadyExists.new(message, sql: sql, binds: binds) when LOCK_NOT_AVAILABLE LockWaitTimeout.new(message, sql: sql, binds: binds) when QUERY_CANCELED diff --git a/activerecord/lib/active_record/database_configurations.rb b/activerecord/lib/active_record/database_configurations.rb index 3e387782f6..fcf13a910c 100644 --- a/activerecord/lib/active_record/database_configurations.rb +++ b/activerecord/lib/active_record/database_configurations.rb @@ -181,12 +181,19 @@ module ActiveRecord end def environment_url_config(env, spec_name, config) - url = ENV["DATABASE_URL"] + url = environment_value_for(spec_name) return unless url ActiveRecord::DatabaseConfigurations::UrlConfig.new(env, spec_name, url, config) end + def environment_value_for(spec_name) + spec_env_key = "#{spec_name.upcase}_DATABASE_URL" + url = ENV[spec_env_key] + url ||= ENV["DATABASE_URL"] if spec_name == "primary" + url + end + def method_missing(method, *args, &blk) case method when :fetch diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb index 20cc987d6e..509f21c9a5 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -187,6 +187,10 @@ module ActiveRecord class NoDatabaseError < StatementInvalid end + # Raised when creating a database if it exists. + class DatabaseAlreadyExists < StatementInvalid + end + # Raised when PostgreSQL returns 'cached plan must not change result type' and # we cannot retry gracefully (e.g. inside a transaction) class PreparedStatementCacheExpired < StatementInvalid @@ -334,7 +338,7 @@ module ActiveRecord # See the following: # # * https://www.postgresql.org/docs/current/static/transaction-iso.html - # * https://dev.mysql.com/doc/refman/5.7/en/error-messages-server.html#error_er_lock_deadlock + # * https://dev.mysql.com/doc/refman/5.7/en/server-error-reference.html#error_er_lock_deadlock class TransactionRollbackError < StatementInvalid end diff --git a/activerecord/lib/active_record/explain_subscriber.rb b/activerecord/lib/active_record/explain_subscriber.rb index a86217abc0..ce209092f5 100644 --- a/activerecord/lib/active_record/explain_subscriber.rb +++ b/activerecord/lib/active_record/explain_subscriber.rb @@ -26,7 +26,7 @@ module ActiveRecord payload[:exception] || payload[:cached] || IGNORED_PAYLOADS.include?(payload[:name]) || - payload[:sql] !~ EXPLAINED_SQLS + !payload[:sql].match?(EXPLAINED_SQLS) end ActiveSupport::Notifications.subscribe("sql.active_record", new) diff --git a/activerecord/lib/active_record/fixture_set/file.rb b/activerecord/lib/active_record/fixture_set/file.rb index f1ea0e022f..b2030a5bb9 100644 --- a/activerecord/lib/active_record/fixture_set/file.rb +++ b/activerecord/lib/active_record/fixture_set/file.rb @@ -29,6 +29,10 @@ module ActiveRecord config_row["model_class"] end + def ignored_fixtures + config_row["ignore"] + end + private def rows @rows ||= raw_rows.reject { |fixture_name, _| fixture_name == "_fixture" } @@ -40,7 +44,7 @@ module ActiveRecord if row row.last else - { 'model_class': nil } + { 'model_class': nil, 'ignore': nil } end end end diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 046ed0e95c..3df4b3c87f 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -420,6 +420,29 @@ module ActiveRecord # # Any fixture labeled "DEFAULTS" is safely ignored. # + # Besides using "DEFAULTS", you can also specify what fixtures will + # be ignored by setting "ignore" in "_fixture" section. + # + # # users.yml + # _fixture: + # ignore: + # - base + # # or use "ignore: base" when there is only one fixture needs to be ignored. + # + # base: &base + # admin: false + # introduction: "This is a default description" + # + # admin: + # <<: *base + # admin: true + # + # visitor: + # <<: *base + # + # In the above example, 'base' will be ignored when creating fixtures. + # This can be used for common attributes inheriting. + # # == Configure the fixture model class # # It's possible to set the fixture's model class directly in the YAML file. @@ -614,7 +637,7 @@ module ActiveRecord end end - attr_reader :table_name, :name, :fixtures, :model_class, :config + attr_reader :table_name, :name, :fixtures, :model_class, :ignored_fixtures, :config def initialize(_, name, class_name, path, config = ActiveRecord::Base) @name = name @@ -647,8 +670,8 @@ module ActiveRecord # Returns a hash of rows to be inserted. The key is the table, the value is # a list of rows to insert to that table. def table_rows - # allow a standard key to be used for doing defaults in YAML - fixtures.delete("DEFAULTS") + # allow specifying fixtures to be ignored by setting `ignore` in `_fixture` section + fixtures.except!(*ignored_fixtures) TableRows.new( table_name, @@ -667,6 +690,21 @@ module ActiveRecord end end + def ignored_fixtures=(base) + @ignored_fixtures = + case base + when Array + base + when String + [base] + else + [] + end + + @ignored_fixtures << "DEFAULTS" unless @ignored_fixtures.include?("DEFAULTS") + @ignored_fixtures.compact + end + # Loads the fixtures from the YAML file at +path+. # If the file sets the +model_class+ and current instance value is not set, # it uses the file value. @@ -678,6 +716,7 @@ module ActiveRecord yaml_files.each_with_object({}) do |file, fixtures| FixtureSet::File.open(file) do |fh| self.model_class ||= fh.model_class if fh.model_class + self.ignored_fixtures ||= fh.ignored_fixtures fh.each do |fixture_name, row| fixtures[fixture_name] = ActiveRecord::Fixture.new(row, model_class) end diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 323b01ab2d..4dfe8655fe 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -566,10 +566,10 @@ module ActiveRecord def becomes(klass) became = klass.allocate became.send(:initialize) - became.instance_variable_set("@attributes", @attributes) - became.instance_variable_set("@mutations_from_database", @mutations_from_database ||= nil) - became.instance_variable_set("@new_record", new_record?) - became.instance_variable_set("@destroyed", destroyed?) + became.instance_variable_set(:@attributes, @attributes) + became.instance_variable_set(:@mutations_from_database, @mutations_from_database ||= nil) + became.instance_variable_set(:@new_record, new_record?) + became.instance_variable_set(:@destroyed, destroyed?) became.errors.copy!(errors) became end @@ -809,7 +809,7 @@ module ActiveRecord self.class.unscoped { self.class.find(id) } end - @attributes = fresh_object.instance_variable_get("@attributes") + @attributes = fresh_object.instance_variable_get(:@attributes) @new_record = false self end diff --git a/activerecord/lib/active_record/query_cache.rb b/activerecord/lib/active_record/query_cache.rb index 43a21e629e..881ba623b8 100644 --- a/activerecord/lib/active_record/query_cache.rb +++ b/activerecord/lib/active_record/query_cache.rb @@ -29,10 +29,10 @@ module ActiveRecord pools = [] ActiveRecord::Base.connection_handlers.each do |key, handler| - pools << handler.connection_pool_list.reject { |p| p.query_cache_enabled }.each { |p| p.enable_query_cache! } + pools.concat(handler.connection_pool_list.reject { |p| p.query_cache_enabled }.each { |p| p.enable_query_cache! }) end - pools.flatten + pools end def self.complete(pools) diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 4d9acc911b..d699e2e21b 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -265,6 +265,8 @@ db_namespace = namespace :db do end abort %{Run `rails db:migrate` to update your database then try again.} end + ensure + ActiveRecord::Base.establish_connection(ActiveRecord::Tasks::DatabaseTasks.env.to_sym) end namespace :abort_if_pending_migrations do diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 6957ba052b..29e2e2cedc 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -1095,26 +1095,44 @@ module ActiveRecord end def build_left_outer_joins(manager, outer_joins, aliases) - buckets = { association_join: valid_association_list(outer_joins) } + buckets = Hash.new { |h, k| h[k] = [] } + buckets[:association_join] = valid_association_list(outer_joins) build_join_query(manager, buckets, Arel::Nodes::OuterJoin, aliases) end def build_joins(manager, joins, aliases) + buckets = Hash.new { |h, k| h[k] = [] } + unless left_outer_joins_values.empty? left_joins = valid_association_list(left_outer_joins_values.flatten) - joins.unshift construct_join_dependency(left_joins, Arel::Nodes::OuterJoin) + buckets[:stashed_join] << construct_join_dependency(left_joins, Arel::Nodes::OuterJoin) end - buckets = joins.group_by do |join| + joins.map! do |join| + if join.is_a?(String) + table.create_string_join(Arel.sql(join.strip)) unless join.blank? + else + join + end + end.compact_blank! + + while joins.first.is_a?(Arel::Nodes::Join) + join_node = joins.shift + if join_node.is_a?(Arel::Nodes::StringJoin) && !buckets[:stashed_join].empty? + buckets[:join_node] << join_node + else + buckets[:leading_join] << join_node + end + end + + joins.each do |join| case join - when String - :string_join when Hash, Symbol, Array - :association_join + buckets[:association_join] << join when ActiveRecord::Associations::JoinDependency - :stashed_join + buckets[:stashed_join] << join when Arel::Nodes::Join - :join_node + buckets[:join_node] << join else raise "unknown class: %s" % join.class.name end @@ -1124,25 +1142,21 @@ module ActiveRecord end def build_join_query(manager, buckets, join_type, aliases) - buckets.default = [] - association_joins = buckets[:association_join] stashed_joins = buckets[:stashed_join] + leading_joins = buckets[:leading_join].tap(&:uniq!) join_nodes = buckets[:join_node].tap(&:uniq!) - string_joins = buckets[:string_join].compact_blank!.map!(&:strip).tap(&:uniq!) - - string_joins.map! { |join| table.create_string_join(Arel.sql(join)) } join_sources = manager.join_sources - join_sources.concat(join_nodes) unless join_nodes.empty? + join_sources.concat(leading_joins) unless leading_joins.empty? unless association_joins.empty? && stashed_joins.empty? - alias_tracker = alias_tracker(join_nodes + string_joins, aliases) + alias_tracker = alias_tracker(leading_joins + join_nodes, aliases) join_dependency = construct_join_dependency(association_joins, join_type) join_sources.concat(join_dependency.join_constraints(stashed_joins, alias_tracker)) end - join_sources.concat(string_joins) unless string_joins.empty? + join_sources.concat(join_nodes) unless join_nodes.empty? end def build_select(arel) diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index 5d1ce19829..300e67b0aa 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -4,7 +4,6 @@ require "active_record/database_configurations" module ActiveRecord module Tasks # :nodoc: - class DatabaseAlreadyExists < StandardError; end # :nodoc: class DatabaseNotSupported < StandardError; end # :nodoc: # ActiveRecord::Tasks::DatabaseTasks is a utility class, which encapsulates diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb index e3efeb75b5..b9a8ccb22c 100644 --- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb @@ -15,12 +15,6 @@ module ActiveRecord establish_connection configuration_without_database connection.create_database configuration["database"], creation_options establish_connection configuration - rescue ActiveRecord::StatementInvalid => error - if connection.error_number(error.cause) == ER_DB_CREATE_EXISTS - raise DatabaseAlreadyExists - else - raise - end end def drop diff --git a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb index 626ffdfdf9..fc37db216d 100644 --- a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb @@ -21,12 +21,6 @@ module ActiveRecord connection.create_database configuration["database"], configuration.merge("encoding" => encoding) establish_connection configuration - rescue ActiveRecord::StatementInvalid => error - if error.cause.is_a?(PG::DuplicateDatabase) - raise DatabaseAlreadyExists - else - raise - end end def drop |