diff options
Diffstat (limited to 'activerecord')
93 files changed, 1580 insertions, 420 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index d1ae41ab97..09af076aa8 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,87 @@ +* Configuration item `config.filter_parameters` could also filter out sensitive value of database column when call `#inspect`. + + ``` + Rails.application.config.filter_parameters += [:credit_card_number] + Account.last.insepct # => #<Account id: 123, credit_card_number: [FILTERED] ...> + ``` + + *Zhang Kang* + +* Deprecate `column_name_length`, `table_name_length`, `columns_per_table`, + `indexes_per_table`, `columns_per_multicolumn_index`, `sql_query_length`, + and `joins_per_query` methods in `DatabaseLimits`. + + *Ryuta Kamizono* + +* `ActiveRecord::Base.configurations` now returns an object. + + `ActiveRecord::Base.configurations` used to return a hash, but this + is an inflexible data model. In order to improve multiple-database + handling in Rails, we've changed this to return an object. Some methods + are provided to make the object behave hash-like in order to ease the + transition process. Since most applications don't manipulate the hash + we've decided to add backwards-compatible functionality that will throw + a deprecation warning if used, however calling `ActiveRecord::Base.configurations` + will use the new version internally and externally. + + For example, the following database.yml... + + ``` + development: + adapter: sqlite3 + database: db/development.sqlite3 + ``` + + Used to become a hash: + + ``` + { "development" => { "adapter" => "sqlite3", "database" => "db/development.sqlite3" } } + ``` + + Is now converted into the following object: + + ``` + #<ActiveRecord::DatabaseConfigurations:0x00007fd1acbdf800 @configurations=[ + #<ActiveRecord::DatabaseConfigurations::HashConfig:0x00007fd1acbded10 @env_name="development", + @spec_name="primary", @config={"adapter"=>"sqlite3", "database"=>"db/development.sqlite3"}> + ] + ``` + + Iterating over the database configurations has also changed. Instead of + calling hash methods on the `configurations` hash directly, a new method `configs_for` has + been provided that allows you to select the correct configuration. `env_name` is a required + argument, `spec_name` is optional as well as passing a block. These return an array of + database config objects for the requested environment and specification name respectively. + + ``` + ActiveRecord::Base.configurations.configs_for("development") + ActiveRecord::Base.configurations.configs_for("development", "primary") + ``` + + *Eileen M. Uchitelle*, *Aaron Patterson* + +* Add database configuration to disable advisory locks. + + ``` + production: + adapter: postgresql + advisory_locks: false + ``` + + *Guo Xiang* + +* SQLite3 adapter `alter_table` method restores foreign keys. + + *Yasuo Honda* + +* Allow `:to_table` option to `invert_remove_foreign_key`. + + Example: + + remove_foreign_key :accounts, to_table: :owners + + *Nikolay Epifanov*, *Rich Chen* + * Add environment & load_config dependency to `bin/rake db:seed` to enable seed load in environments without Rails and custom DB configuration @@ -64,8 +148,8 @@ *Eddie Lebow* -* Add ActiveRecord::Base.create_or_find_by/! to deal with the SELECT/INSERT race condition in - ActiveRecord::Base.find_or_create_by/! by leaning on unique constraints in the database. +* Add `ActiveRecord::Base.create_or_find_by`/`!` to deal with the SELECT/INSERT race condition in + `ActiveRecord::Base.find_or_create_by`/`!` by leaning on unique constraints in the database. *DHH* diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index d198466dbf..d43378c64f 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -40,7 +40,6 @@ module ActiveRecord autoload :Core autoload :ConnectionHandling autoload :CounterCache - autoload :DatabaseConfigurations autoload :DynamicMatchers autoload :Enum autoload :InternalMetadata diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb index b5fb0092d7..0166ed98ca 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -34,7 +34,7 @@ module ActiveRecord::Associations::Builder # :nodoc: foreign_key = reflection.foreign_key cache_column = reflection.counter_cache_column - if (@_after_replace_counter_called ||= false) + if @_after_replace_counter_called ||= false @_after_replace_counter_called = false elsif association(reflection.name).target_changed? if reflection.polymorphic? diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index e4b8b1a330..701f19a6ae 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -31,7 +31,7 @@ module ActiveRecord end } - BLACKLISTED_CLASS_METHODS = %w(private public protected allocate new name parent superclass) + RESTRICTED_CLASS_METHODS = %w(private public protected allocate new name parent superclass) class GeneratedAttributeMethods < Module #:nodoc: include Mutex_m @@ -123,7 +123,7 @@ module ActiveRecord # A class method is 'dangerous' if it is already (re)defined by Active Record, but # not by any ancestors. (So 'puts' is not dangerous but 'new' is.) def dangerous_class_method?(method_name) - BLACKLISTED_CLASS_METHODS.include?(method_name.to_s) || class_method_defined_within?(method_name, Base) + RESTRICTED_CLASS_METHODS.include?(method_name.to_s) || class_method_defined_within?(method_name, Base) end def class_method_defined_within?(name, klass, superklass = klass.superclass) # :nodoc: @@ -167,12 +167,14 @@ module ActiveRecord end end - # Regexp whitelist. Matches the following: + # Regexp for column names (with or without a table name prefix). Matches + # the following: # "#{table_name}.#{column_name}" # "#{column_name}" - COLUMN_NAME_WHITELIST = /\A(?:\w+\.)?\w+\z/i + COLUMN_NAME = /\A(?:\w+\.)?\w+\z/i - # Regexp whitelist. Matches the following: + # Regexp for column names with order (with or without a table name + # prefix, with or without various order modifiers). Matches the following: # "#{table_name}.#{column_name}" # "#{table_name}.#{column_name} #{direction}" # "#{table_name}.#{column_name} #{direction} NULLS FIRST" @@ -181,7 +183,7 @@ module ActiveRecord # "#{column_name} #{direction}" # "#{column_name} #{direction} NULLS FIRST" # "#{column_name} NULLS LAST" - COLUMN_NAME_ORDER_WHITELIST = / + COLUMN_NAME_WITH_ORDER = / \A (?:\w+\.)? \w+ @@ -190,12 +192,12 @@ module ActiveRecord \z /ix - def enforce_raw_sql_whitelist(args, whitelist: COLUMN_NAME_WHITELIST) # :nodoc: + def disallow_raw_sql!(args, permit: COLUMN_NAME) # :nodoc: unexpected = args.reject do |arg| arg.kind_of?(Arel::Node) || arg.is_a?(Arel::Nodes::SqlLiteral) || arg.is_a?(Arel::Attributes::Attribute) || - arg.to_s.split(/\s*,\s*/).all? { |part| whitelist.match?(part) } + arg.to_s.split(/\s*,\s*/).all? { |part| permit.match?(part) } end return if unexpected.none? @@ -449,14 +451,6 @@ module ActiveRecord defined?(@attributes) && @attributes.key?(attr_name) end - def attributes_with_values_for_create(attribute_names) - attributes_with_values(attributes_for_create(attribute_names)) - end - - def attributes_with_values_for_update(attribute_names) - attributes_with_values(attributes_for_update(attribute_names)) - end - def attributes_with_values(attribute_names) attribute_names.each_with_object({}) do |name, attrs| attrs[name] = _read_attribute(name) @@ -465,7 +459,8 @@ module ActiveRecord # Filters the primary keys and readonly attributes from the attribute names. def attributes_for_update(attribute_names) - attribute_names.reject do |name| + attribute_names &= self.class.column_names + attribute_names.delete_if do |name| readonly_attribute?(name) end end @@ -473,7 +468,8 @@ module ActiveRecord # Filters out the primary keys, from the attribute names, when the primary # key is to be generated (e.g. the id attribute has no value). def attributes_for_create(attribute_names) - attribute_names.reject do |name| + attribute_names &= self.class.column_names + attribute_names.delete_if do |name| pk_attribute?(name) && id.nil? end end diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index 233ee29fac..ebc2252c50 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -16,9 +16,6 @@ module ActiveRecord class_attribute :partial_writes, instance_writer: false, default: true - after_create { changes_applied } - after_update { changes_applied } - # Attribute methods for "changed in last call to save?" attribute_method_affix(prefix: "saved_change_to_", suffix: "?") attribute_method_prefix("saved_change_to_") @@ -167,16 +164,20 @@ module ActiveRecord result end - def _update_record(*) - partial_writes? ? super(keys_for_partial_write) : super + def _update_record(attribute_names = attribute_names_for_partial_writes) + affected_rows = super + changes_applied + affected_rows end - def _create_record(*) - partial_writes? ? super(keys_for_partial_write) : super + def _create_record(attribute_names = attribute_names_for_partial_writes) + id = super + changes_applied + id end - def keys_for_partial_write - changed_attribute_names_to_save & self.class.column_names + def attribute_names_for_partial_writes + partial_writes? ? changed_attribute_names_to_save : attribute_names end end end diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index a405f05e0b..783a8366ce 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -392,7 +392,7 @@ module ActiveRecord records -= records_to_destroy end - records.each_with_index do |record, index| + records.each do |record| next if record.destroyed? saved = true @@ -401,11 +401,11 @@ module ActiveRecord if autosave saved = association.insert_record(record, false) elsif !reflection.nested? + association_saved = association.insert_record(record) + if reflection.validate? - valid = association_valid?(reflection, record, index) - saved = valid ? association.insert_record(record, false) : false - else - association.insert_record(record) + errors.add(reflection.name) unless association_saved + saved = association_saved end end elsif autosave diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 5169f312f5..db097cb930 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -22,6 +22,7 @@ require "active_record/explain_subscriber" require "active_record/relation/delegation" require "active_record/attributes" require "active_record/type_caster" +require "active_record/database_configurations" module ActiveRecord #:nodoc: # = Active Record @@ -291,7 +292,6 @@ module ActiveRecord #:nodoc: extend Aggregations::ClassMethods include Core - include DatabaseConfigurations include Persistence include ReadonlyAttributes include ModelSchema diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index f721e91203..e977d36cb9 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -188,7 +188,9 @@ module ActiveRecord t0 = Time.now elapsed = 0 loop do - @cond.wait(timeout - elapsed) + ActiveSupport::Dependencies.interlock.permit_concurrent_loads do + @cond.wait(timeout - elapsed) + end return remove if any? diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb index 7a9e7add24..ad148efcfe 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "active_support/deprecation" + module ActiveRecord module ConnectionAdapters # :nodoc: module DatabaseLimits @@ -12,11 +14,13 @@ module ActiveRecord def column_name_length 64 end + deprecate :column_name_length # Returns the maximum length of a table name. def table_name_length 64 end + deprecate :table_name_length # Returns the maximum allowed length for an index name. This # limit is enforced by \Rails and is less than or equal to @@ -36,16 +40,19 @@ module ActiveRecord def columns_per_table 1024 end + deprecate :columns_per_table # Returns the maximum number of indexes per table. def indexes_per_table 16 end + deprecate :indexes_per_table # Returns the maximum number of columns in a multicolumn index. def columns_per_multicolumn_index 16 end + deprecate :columns_per_multicolumn_index # Returns the maximum number of elements in an IN (x,y,z) clause. # +nil+ means no limit. @@ -57,11 +64,13 @@ module ActiveRecord def sql_query_length 1048575 end + deprecate :sql_query_length # Returns maximum number of joins in a single query. def joins_per_query 256 end + deprecate :joins_per_query end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index 41553cfa83..1d36c3c8b1 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -259,7 +259,9 @@ module ActiveRecord attr_reader :transaction_manager #:nodoc: - delegate :within_new_transaction, :open_transactions, :current_transaction, :begin_transaction, :commit_transaction, :rollback_transaction, to: :transaction_manager + delegate :within_new_transaction, :open_transactions, :current_transaction, :begin_transaction, + :commit_transaction, :rollback_transaction, :materialize_transactions, + :disable_lazy_transactions!, :enable_lazy_transactions!, to: :transaction_manager def transaction_open? current_transaction.open? diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index 3be0906f2a..4a6e915b66 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -522,6 +522,9 @@ module ActiveRecord # Specifies the precision for the <tt>:decimal</tt> and <tt>:numeric</tt> columns. # * <tt>:scale</tt> - # Specifies the scale for the <tt>:decimal</tt> and <tt>:numeric</tt> columns. + # * <tt>:collation</tt> - + # Specifies the collation for a <tt>:string</tt> or <tt>:text</tt> column. If not specified, the + # column will have the same collation as the table. # * <tt>:comment</tt> - # Specifies the comment for the column. This option is ignored by some backends. # @@ -599,6 +602,7 @@ module ActiveRecord # The +type+ and +options+ parameters will be ignored if present. It can be helpful # to provide these in a migration's +change+ method so it can be reverted. # In that case, +type+ and +options+ will be used by #add_column. + # Indexes on the column are automatically removed. def remove_column(table_name, column_name, type = nil, options = {}) execute "ALTER TABLE #{quote_table_name(table_name)} #{remove_column_for_alter(table_name, column_name, type, options)}" end @@ -980,11 +984,18 @@ module ActiveRecord # # remove_foreign_key :accounts, column: :owner_id # + # Removes the foreign key on +accounts.owner_id+. + # + # remove_foreign_key :accounts, to_table: :owners + # # Removes the foreign key named +special_fk_name+ on the +accounts+ table. # # remove_foreign_key :accounts, name: :special_fk_name # - # The +options+ hash accepts the same keys as SchemaStatements#add_foreign_key. + # The +options+ hash accepts the same keys as SchemaStatements#add_foreign_key + # with an addition of + # [<tt>:to_table</tt>] + # The name of the table that contains the referenced primary key. def remove_foreign_key(from_table, options_or_to_table = {}) return unless supports_foreign_keys? diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb index b59df2fff7..564b226b39 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb @@ -91,12 +91,14 @@ module ActiveRecord end class Transaction #:nodoc: - attr_reader :connection, :state, :records, :savepoint_name + attr_reader :connection, :state, :records, :savepoint_name, :isolation_level def initialize(connection, options, run_commit_callbacks: false) @connection = connection @state = TransactionState.new @records = [] + @isolation_level = options[:isolation] + @materialized = false @joinable = options.fetch(:joinable, true) @run_commit_callbacks = run_commit_callbacks end @@ -105,6 +107,14 @@ module ActiveRecord records << record end + def materialize! + @materialized = true + end + + def materialized? + @materialized + end + def rollback_records ite = records.uniq while record = ite.shift @@ -141,24 +151,30 @@ module ActiveRecord end class SavepointTransaction < Transaction - def initialize(connection, savepoint_name, parent_transaction, options, *args) - super(connection, options, *args) + def initialize(connection, savepoint_name, parent_transaction, *args) + super(connection, *args) parent_transaction.state.add_child(@state) - if options[:isolation] + if isolation_level raise ActiveRecord::TransactionIsolationError, "cannot set transaction isolation in a nested transaction" end - connection.create_savepoint(@savepoint_name = savepoint_name) + + @savepoint_name = savepoint_name + end + + def materialize! + connection.create_savepoint(savepoint_name) + super end def rollback - connection.rollback_to_savepoint(savepoint_name) + connection.rollback_to_savepoint(savepoint_name) if materialized? @state.rollback! end def commit - connection.release_savepoint(savepoint_name) + connection.release_savepoint(savepoint_name) if materialized? @state.commit! end @@ -166,22 +182,23 @@ module ActiveRecord end class RealTransaction < Transaction - def initialize(connection, options, *args) - super - if options[:isolation] - connection.begin_isolated_db_transaction(options[:isolation]) + def materialize! + if isolation_level + connection.begin_isolated_db_transaction(isolation_level) else connection.begin_db_transaction end + + super end def rollback - connection.rollback_db_transaction + connection.rollback_db_transaction if materialized? @state.full_rollback! end def commit - connection.commit_db_transaction + connection.commit_db_transaction if materialized? @state.full_commit! end end @@ -190,6 +207,9 @@ module ActiveRecord def initialize(connection) @stack = [] @connection = connection + @has_unmaterialized_transactions = false + @materializing_transactions = false + @lazy_transactions_enabled = true end def begin_transaction(options = {}) @@ -203,11 +223,41 @@ module ActiveRecord run_commit_callbacks: run_commit_callbacks) end + transaction.materialize! unless @connection.supports_lazy_transactions? && lazy_transactions_enabled? @stack.push(transaction) + @has_unmaterialized_transactions = true if @connection.supports_lazy_transactions? transaction end end + def disable_lazy_transactions! + materialize_transactions + @lazy_transactions_enabled = false + end + + def enable_lazy_transactions! + @lazy_transactions_enabled = true + end + + def lazy_transactions_enabled? + @lazy_transactions_enabled + end + + def materialize_transactions + return if @materializing_transactions + return unless @has_unmaterialized_transactions + + @connection.lock.synchronize do + begin + @materializing_transactions = true + @stack.each { |t| t.materialize! unless t.materialized? } + ensure + @materializing_transactions = false + end + @has_unmaterialized_transactions = false + end + end + def commit_transaction @connection.lock.synchronize do transaction = @stack.last diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index a4748dbeda..a62651daff 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -80,6 +80,8 @@ module ActiveRecord attr_reader :schema_cache, :owner, :logger, :prepared_statements, :lock alias :in_use? :owner + set_callback :checkin, :after, :enable_lazy_transactions! + def self.type_cast_config_to_integer(config) if config.is_a?(Integer) config @@ -119,6 +121,14 @@ module ActiveRecord else @prepared_statements = false end + + @advisory_locks_enabled = self.class.type_cast_config_to_boolean( + config.fetch(:advisory_locks, true) + ) + end + + def replica? + @config[:replica] || false end def migrations_paths # :nodoc: @@ -338,6 +348,10 @@ module ActiveRecord false end + def supports_lazy_transactions? + false + end + # This is meant to be implemented by the adapters that support extensions def disable_extension(name) end @@ -449,6 +463,7 @@ module ActiveRecord # This is useful for when you need to call a proprietary method such as # PostgreSQL's lo_* methods. def raw_connection + disable_lazy_transactions! @connection end 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 9de8242a58..ad045f85ef 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -111,7 +111,7 @@ module ActiveRecord end def supports_advisory_locks? - true + @advisory_locks_enabled end def get_advisory_lock(lock_name, timeout = 0) # :nodoc: @@ -180,6 +180,8 @@ module ActiveRecord # Executes the SQL statement in the context of this connection. def execute(sql, name = nil) + materialize_transactions + log(sql, name) do ActiveSupport::Dependencies.interlock.permit_concurrent_loads do @connection.query(sql) diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb index 204691006c..2e7a78215a 100644 --- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb @@ -114,8 +114,7 @@ module ActiveRecord class Resolver # :nodoc: attr_reader :configurations - # Accepts a hash two layers deep, keys on the first layer represent - # environments such as "production". Keys must be strings. + # Accepts a list of db config objects. def initialize(configurations) @configurations = configurations end @@ -136,33 +135,14 @@ module ActiveRecord # Resolver.new(configurations).resolve(:production) # # => { "host" => "localhost", "database" => "foo", "adapter" => "postgresql" } # - def resolve(config) - if config - resolve_connection config - elsif env = ActiveRecord::ConnectionHandling::RAILS_ENV.call - resolve_symbol_connection env.to_sym + def resolve(config_or_env, pool_name = nil) + if config_or_env + resolve_connection config_or_env, pool_name else raise AdapterNotSpecified end end - # Expands each key in @configurations hash into fully resolved hash - def resolve_all - config = configurations.dup - - if env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call - env_config = config[env] if config[env].is_a?(Hash) && !(config[env].key?("adapter") || config[env].key?("url")) - end - - config.merge! env_config if env_config - - config.each do |key, value| - config[key] = resolve(value) if value - end - - config - end - # Returns an instance of ConnectionSpecification for a given adapter. # Accepts a hash one layer deep that contains all connection information. # @@ -176,7 +156,9 @@ module ActiveRecord # # => { "host" => "localhost", "database" => "foo", "adapter" => "sqlite3" } # def spec(config) - spec = resolve(config).symbolize_keys + pool_name = config if config.is_a?(Symbol) + + spec = resolve(config, pool_name).symbolize_keys raise(AdapterNotSpecified, "database configuration does not specify adapter") unless spec.key?(:adapter) @@ -211,7 +193,6 @@ module ActiveRecord end private - # Returns fully resolved connection, accepts hash, string or symbol. # Always returns a hash. # @@ -232,29 +213,42 @@ module ActiveRecord # Resolver.new({}).resolve_connection("postgresql://localhost/foo") # # => { "host" => "localhost", "database" => "foo", "adapter" => "postgresql" } # - def resolve_connection(spec) - case spec + def resolve_connection(config_or_env, pool_name = nil) + case config_or_env when Symbol - resolve_symbol_connection spec + resolve_symbol_connection config_or_env, pool_name when String - resolve_url_connection spec + resolve_url_connection config_or_env when Hash - resolve_hash_connection spec + resolve_hash_connection config_or_env + else + resolve_connection config_or_env end end - # Takes the environment such as +:production+ or +:development+. + # Takes the environment such as +:production+ or +:development+ and a + # pool name the corresponds to the name given by the connection pool + # to the connection. That pool name is merged into the hash with the + # name key. + # # This requires that the @configurations was initialized with a key that # matches. # - # Resolver.new("production" => {}).resolve_symbol_connection(:production) - # # => {} + # configurations = #<ActiveRecord::DatabaseConfigurations:0x00007fd9fdace3e0 + # @configurations=[ + # #<ActiveRecord::DatabaseConfigurations::HashConfig:0x00007fd9fdace250 + # @env_name="production", @spec_name="primary", @config={"database"=>"my_db"}> + # ]> # - def resolve_symbol_connection(spec) - if config = configurations[spec.to_s] - resolve_connection(config).merge("name" => spec.to_s) + # Resolver.new(configurations).resolve_symbol_connection(:production, "primary") + # # => { "database" => "my_db" } + def resolve_symbol_connection(env_name, pool_name) + db_config = configurations.find_db_config(env_name) + + if db_config + resolve_connection(db_config.config).merge("name" => pool_name.to_s) else - raise(AdapterNotSpecified, "'#{spec}' database is not configured. Available: #{configurations.keys.inspect}") + raise(AdapterNotSpecified, "'#{env_name}' database is not configured. Available: #{configurations.configurations.map(&:env_name).join(", ")}") end end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb index d89eeb7f54..684c7042a7 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb @@ -29,6 +29,8 @@ module ActiveRecord end def exec_query(sql, name = "SQL", binds = [], prepare: false) + materialize_transactions + if without_prepared_statement?(binds) execute_and_free(sql, name) do |result| ActiveRecord::Result.new(result.fields, result.to_a) if result @@ -41,6 +43,8 @@ module ActiveRecord end def exec_delete(sql, name = nil, binds = []) + materialize_transactions + if without_prepared_statement?(binds) execute_and_free(sql, name) { @connection.affected_rows } else diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index 544d720428..92f15de219 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -58,6 +58,10 @@ module ActiveRecord true end + def supports_lazy_transactions? + true + end + # HELPER METHODS =========================================== def each_hash(result) # :nodoc: diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb index 8db2a645af..6bd6b67165 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb @@ -58,6 +58,8 @@ module ActiveRecord # Queries the database and returns the results in an Array-like object def query(sql, name = nil) #:nodoc: + materialize_transactions + log(sql, name) do ActiveSupport::Dependencies.interlock.permit_concurrent_loads do result_as_array @connection.async_exec(sql) @@ -70,6 +72,8 @@ module ActiveRecord # Note: the PG::Result object is manually memory managed; if you don't # need it specifically, you may want consider the <tt>exec_query</tt> wrapper. def execute(sql, name = nil) + materialize_transactions + log(sql, name) do ActiveSupport::Dependencies.interlock.permit_concurrent_loads do @connection.async_exec(sql) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb index 231278c184..79351bc3a4 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "active_support/core_ext/array/extract" + module ActiveRecord module ConnectionAdapters module PostgreSQL @@ -16,12 +18,12 @@ module ActiveRecord def run(records) nodes = records.reject { |row| @store.key? row["oid"].to_i } - mapped, nodes = nodes.partition { |row| @store.key? row["typname"] } - ranges, nodes = nodes.partition { |row| row["typtype"] == "r".freeze } - enums, nodes = nodes.partition { |row| row["typtype"] == "e".freeze } - domains, nodes = nodes.partition { |row| row["typtype"] == "d".freeze } - arrays, nodes = nodes.partition { |row| row["typinput"] == "array_in".freeze } - composites, nodes = nodes.partition { |row| row["typelem"].to_i != 0 } + mapped = nodes.extract! { |row| @store.key? row["typname"] } + ranges = nodes.extract! { |row| row["typtype"] == "r".freeze } + enums = nodes.extract! { |row| row["typtype"] == "e".freeze } + domains = nodes.extract! { |row| row["typtype"] == "d".freeze } + arrays = nodes.extract! { |row| row["typinput"] == "array_in".freeze } + composites = nodes.extract! { |row| row["typelem"].to_i != 0 } mapped.each { |row| register_mapped_type(row) } enums.each { |row| register_enum_type(row) } 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 26b8113b0d..00da7690a2 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -700,6 +700,11 @@ module ActiveRecord sql end + def add_column_for_alter(table_name, column_name, type, options = {}) + return super unless options.key?(:comment) + [super, Proc.new { change_column_comment(table_name, column_name, options[:comment]) }] + end + def change_column_for_alter(table_name, column_name, type, options = {}) sqls = [change_column_sql(table_name, column_name, type, options)] sqls << change_column_default_for_alter(table_name, column_name, options[:default]) if options.key?(:default) @@ -708,7 +713,6 @@ module ActiveRecord sqls end - # Changes the default value of a table column. def change_column_default_for_alter(table_name, column_name, default_or_changes) # :nodoc: column = column_for(table_name, column_name) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index fdf6f75108..30e651ee63 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -298,7 +298,7 @@ module ActiveRecord end def supports_advisory_locks? - true + @advisory_locks_enabled end def supports_explain? @@ -326,6 +326,10 @@ module ActiveRecord postgresql_version >= 90400 end + def supports_lazy_transactions? + true + end + def get_advisory_lock(lock_id) # :nodoc: unless lock_id.is_a?(Integer) && lock_id.bit_length <= 63 raise(ArgumentError, "PostgreSQL requires advisory lock ids to be a signed 64 bit integer") @@ -597,6 +601,8 @@ module ActiveRecord end def exec_no_cache(sql, name, binds) + materialize_transactions + type_casted_binds = type_casted_binds(binds) log(sql, name, binds, type_casted_binds) do ActiveSupport::Dependencies.interlock.permit_concurrent_loads do @@ -606,6 +612,8 @@ module ActiveRecord end def exec_cache(sql, name, binds) + materialize_transactions + stmt_key = prepare_statement(sql) type_casted_binds = type_casted_binds(binds) diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index bee74dc33d..efe454fa7f 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -186,6 +186,10 @@ module ActiveRecord true end + def supports_lazy_transactions? + true + end + # REFERENTIAL INTEGRITY ==================================== def disable_referential_integrity # :nodoc: @@ -212,6 +216,8 @@ module ActiveRecord end def exec_query(sql, name = nil, binds = [], prepare: false) + materialize_transactions + type_casted_binds = type_casted_binds(binds) log(sql, name, binds, type_casted_binds) do @@ -252,6 +258,8 @@ module ActiveRecord end def execute(sql, name = nil) #:nodoc: + materialize_transactions + log(sql, name) do ActiveSupport::Dependencies.interlock.permit_concurrent_loads do @connection.execute(sql) @@ -409,12 +417,23 @@ module ActiveRecord def alter_table(table_name, options = {}) altered_table_name = "a#{table_name}" - caller = lambda { |definition| yield definition if block_given? } + foreign_keys = foreign_keys(table_name) + + caller = lambda do |definition| + rename = options[:rename] || {} + foreign_keys.each do |fk| + if column = rename[fk.options[:column]] + fk.options[:column] = column + end + definition.foreign_key(fk.to_table, fk.options) + end + + yield definition if block_given? + end transaction do disable_referential_integrity do - move_table(table_name, altered_table_name, - options.merge(temporary: true)) + move_table(table_name, altered_table_name, options.merge(temporary: true)) move_table(altered_table_name, table_name, &caller) end end @@ -446,6 +465,7 @@ module ActiveRecord primary_key: column_name == from_primary_key ) end + yield @definition if block_given? end copy_table_indexes(from, to, options[:rename] || {}) diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb index ee0e651912..18114f9e1c 100644 --- a/activerecord/lib/active_record/connection_handling.rb +++ b/activerecord/lib/active_record/connection_handling.rb @@ -46,45 +46,18 @@ module ActiveRecord # # The exceptions AdapterNotSpecified, AdapterNotFound and +ArgumentError+ # may be returned on an error. - def establish_connection(config = nil) + def establish_connection(config_or_env = nil) raise "Anonymous class is not allowed." unless name - config ||= DEFAULT_ENV.call.to_sym - spec_name = self == Base ? "primary" : name - self.connection_specification_name = spec_name + config_or_env ||= DEFAULT_ENV.call.to_sym + pool_name = self == Base ? "primary" : name + self.connection_specification_name = pool_name resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(Base.configurations) - spec = resolver.resolve(config).symbolize_keys - spec[:name] = spec_name + config_hash = resolver.resolve(config_or_env, pool_name).symbolize_keys + config_hash[:name] = pool_name - # use the primary config if a config is not passed in and - # it's a three tier config - spec = spec[spec_name.to_sym] if spec[spec_name.to_sym] - - connection_handler.establish_connection(spec) - end - - class MergeAndResolveDefaultUrlConfig # :nodoc: - def initialize(raw_configurations) - @raw_config = raw_configurations.dup - @env = DEFAULT_ENV.call.to_s - end - - # Returns fully resolved connection hashes. - # Merges connection information from `ENV['DATABASE_URL']` if available. - def resolve - ConnectionAdapters::ConnectionSpecification::Resolver.new(config).resolve_all - end - - private - def config - @raw_config.dup.tap do |cfg| - if url = ENV["DATABASE_URL"] - cfg[@env] ||= {} - cfg[@env]["url"] ||= url - end - end - end + connection_handler.establish_connection(config_hash) end # Returns the connection currently associated with the class. This can diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index c983bc0d93..82cf7563a2 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -8,6 +8,8 @@ module ActiveRecord module Core extend ActiveSupport::Concern + FILTERED = "[FILTERED]" # :nodoc: + included do ## # :singleton-method: @@ -26,7 +28,7 @@ module ActiveRecord ## # Contains the database configuration - as is typically stored in config/database.yml - - # as a Hash. + # as an ActiveRecord::DatabaseConfigurations object. # # For example, the following database.yml... # @@ -40,22 +42,18 @@ module ActiveRecord # # ...would result in ActiveRecord::Base.configurations to look like this: # - # { - # 'development' => { - # 'adapter' => 'sqlite3', - # 'database' => 'db/development.sqlite3' - # }, - # 'production' => { - # 'adapter' => 'sqlite3', - # 'database' => 'db/production.sqlite3' - # } - # } + # #<ActiveRecord::DatabaseConfigurations:0x00007fd1acbdf800 @configurations=[ + # #<ActiveRecord::DatabaseConfigurations::HashConfig:0x00007fd1acbded10 @env_name="development", + # @spec_name="primary", @config={"adapter"=>"sqlite3", "database"=>"db/development.sqlite3"}>, + # #<ActiveRecord::DatabaseConfigurations::HashConfig:0x00007fd1acbdea90 @env_name="production", + # @spec_name="primary", @config={"adapter"=>"mysql2", "database"=>"db/production.sqlite3"}> + # ]> def self.configurations=(config) - @@configurations = ActiveRecord::ConnectionHandling::MergeAndResolveDefaultUrlConfig.new(config).resolve + @@configurations = ActiveRecord::DatabaseConfigurations.new(config) end self.configurations = {} - # Returns fully resolved configurations hash + # Returns fully resolved ActiveRecord::DatabaseConfigurations object def self.configurations @@configurations end @@ -127,6 +125,10 @@ module ActiveRecord class_attribute :default_connection_handler, instance_writer: false + ## + # Specifies columns which don't want to be exposed while calling #inspect + class_attribute :filter_attributes, instance_writer: false, default: [] + def self.connection_handler ActiveRecord::RuntimeRegistry.connection_handler || default_connection_handler end @@ -491,12 +493,17 @@ module ActiveRecord # Returns the contents of the record as a nicely formatted string. def inspect + filter_attributes = self.filter_attributes.map(&:to_s).to_set # We check defined?(@attributes) not to issue warnings if the object is # allocated but not initialized. inspection = if defined?(@attributes) && @attributes self.class.attribute_names.collect do |name| if has_attribute?(name) - "#{name}: #{attribute_for_inspect(name)}" + if filter_attributes.include?(name) && !read_attribute(name).nil? + "#{name}: #{ActiveRecord::Core::FILTERED}" + else + "#{name}: #{attribute_for_inspect(name)}" + end end end.compact.join(", ") else @@ -510,6 +517,7 @@ module ActiveRecord # when pp is required. def pretty_print(pp) return super if custom_inspect_method_defined? + filter_attributes = self.filter_attributes.map(&:to_s).to_set pp.object_address_group(self) do if defined?(@attributes) && @attributes column_names = self.class.column_names.select { |name| has_attribute?(name) || new_record? } @@ -520,7 +528,11 @@ module ActiveRecord pp.text column_name pp.text ":" pp.breakable - pp.pp column_value + if filter_attributes.include?(column_name) && !column_value.nil? + pp.text ActiveRecord::Core::FILTERED + else + pp.pp column_value + end end end else diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb index c7f0077a76..aad30b40ea 100644 --- a/activerecord/lib/active_record/counter_cache.rb +++ b/activerecord/lib/active_record/counter_cache.rb @@ -159,8 +159,7 @@ module ActiveRecord end private - - def _create_record(*) + def _create_record(attribute_names = self.attribute_names) id = super each_counter_cached_associations do |association| diff --git a/activerecord/lib/active_record/database_configurations.rb b/activerecord/lib/active_record/database_configurations.rb index ffeed45030..a94f46d07f 100644 --- a/activerecord/lib/active_record/database_configurations.rb +++ b/activerecord/lib/active_record/database_configurations.rb @@ -1,63 +1,185 @@ # frozen_string_literal: true +require "active_record/database_configurations/database_config" +require "active_record/database_configurations/hash_config" +require "active_record/database_configurations/url_config" + module ActiveRecord - module DatabaseConfigurations # :nodoc: - class DatabaseConfig - attr_reader :env_name, :spec_name, :config + # ActiveRecord::DatabaseConfigurations returns an array of DatabaseConfig + # objects (either a HashConfig or UrlConfig) that are constructed from the + # application's database configuration hash or url string. + class DatabaseConfigurations + attr_reader :configurations + + def initialize(configurations = {}) + @configurations = build_configs(configurations) + end - def initialize(env_name, spec_name, config) - @env_name = env_name - @spec_name = spec_name - @config = config + # Collects the configs for the environment and optionally the specification + # name passed in. To include replica configurations pass `include_replicas: true`. + # + # If a spec name is provided a single DatabaseConfiguration object will be + # returned, otherwise an array of DatabaseConfiguration objects will be + # returned that corresponds with the environment and type requested. + # + # Options: + # + # <tt>env_name:</tt> The environment name. Defaults to nil which will collect + # configs for all environments. + # <tt>spec_name:</tt> The specification name (ie primary, animals, etc.). Defaults + # to +nil+. + # <tt>include_replicas:</tt> Determines whether to include replicas in the + # the returned list. Most of the time we're only iterating over the write + # connection (i.e. migrations don't need to run for the write and read connection). + # Defaults to +false+. + def configs_for(env_name: nil, spec_name: nil, include_replicas: false) + configs = env_with_configs(env_name) + + unless include_replicas + configs = configs.select do |db_config| + !db_config.replica? + end + end + + if spec_name + configs.find do |db_config| + db_config.spec_name == spec_name + end + else + configs end end - # Selects the config for the specified environment and specification name + # Returns the config hash that corresponds with the environment + # + # If the application has multiple databases `default_hash` will + # the first config hash for the environment. + # + # { database: "my_db", adapter: "mysql2" } + def default_hash(env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call.to_s) + default = find_db_config(env) + default.config if default + end + alias :[] :default_hash + + # Returns a single DatabaseConfig object based on the requested environment. # - # For example if passed :development, and :animals it will select the database - # under the :development and :animals configuration level - def self.config_for_env_and_spec(environment, specification_name, configs = ActiveRecord::Base.configurations) # :nodoc: - configs_for(environment, configs).find do |db_config| - db_config.spec_name == specification_name + # If the application has multiple databases `select_db_config` will return + # the first DatabaseConfig for the environment. + def find_db_config(env) + configurations.find do |db_config| + db_config.env_name == env.to_s || + (db_config.for_current_env? && db_config.spec_name == env.to_s) end end - # Collects the configs for the environment passed in. + # Returns the DatabaseConfig object as a Hash. + def to_h + configs = configurations.reverse.inject({}) do |memo, db_config| + memo.merge(db_config.to_legacy_hash) + end + + Hash[configs.to_a.reverse] + end + + # Checks if the application's configurations are empty. # - # If a block is given returns the specification name and configuration - # otherwise returns an array of DatabaseConfig structs for the environment. - def self.configs_for(env, configs = ActiveRecord::Base.configurations, &blk) # :nodoc: - env_with_configs = db_configs(configs).select do |db_config| - db_config.env_name == env + # Aliased to blank? + def empty? + configurations.empty? + end + alias :blank? :empty? + + private + def env_with_configs(env = nil) + if env + configurations.select { |db_config| db_config.env_name == env } + else + configurations + end end - if block_given? - env_with_configs.each do |env_with_config| - yield env_with_config.spec_name, env_with_config.config + def build_configs(configs) + return configs.configurations if configs.is_a?(DatabaseConfigurations) + + build_db_config = configs.each_pair.flat_map do |env_name, config| + walk_configs(env_name, "primary", config) + end.compact + + if url = ENV["DATABASE_URL"] + build_url_config(url, build_db_config) + else + build_db_config end - else - env_with_configs end - end - # Given an env, spec and config creates DatabaseConfig structs with - # each attribute set. - def self.walk_configs(env_name, spec_name, config) # :nodoc: - if config["database"] || config["url"] || config["adapter"] - DatabaseConfig.new(env_name, spec_name, config) - else - config.each_pair.map do |sub_spec_name, sub_config| - walk_configs(env_name, sub_spec_name, sub_config) + def walk_configs(env_name, spec_name, config) + case config + when String + build_db_config_from_string(env_name, spec_name, config) + when Hash + build_db_config_from_hash(env_name, spec_name, config) end end - end - # Walks all the configs passed in and returns an array - # of DatabaseConfig structs for each configuration. - def self.db_configs(configs = ActiveRecord::Base.configurations) # :nodoc: - configs.each_pair.flat_map do |env_name, config| - walk_configs(env_name, "primary", config) + def build_db_config_from_string(env_name, spec_name, config) + begin + url = config + uri = URI.parse(url) + if uri.try(:scheme) + ActiveRecord::DatabaseConfigurations::UrlConfig.new(env_name, spec_name, url) + end + rescue URI::InvalidURIError + ActiveRecord::DatabaseConfigurations::HashConfig.new(env_name, spec_name, config) + end + end + + def build_db_config_from_hash(env_name, spec_name, config) + if url = config["url"] + config_without_url = config.dup + config_without_url.delete "url" + ActiveRecord::DatabaseConfigurations::UrlConfig.new(env_name, spec_name, url, config_without_url) + elsif config["database"] || (config.size == 1 && config.values.all? { |v| v.is_a? String }) + ActiveRecord::DatabaseConfigurations::HashConfig.new(env_name, spec_name, config) + else + config.each_pair.map do |sub_spec_name, sub_config| + walk_configs(env_name, sub_spec_name, sub_config) + end + end + end + + def build_url_config(url, configs) + env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call.to_s + + if original_config = configs.find(&:for_current_env?) + if original_config.url_config? + configs + else + configs.map do |config| + ActiveRecord::DatabaseConfigurations::UrlConfig.new(env, config.spec_name, url, config.config) + end + end + else + configs + [ActiveRecord::DatabaseConfigurations::UrlConfig.new(env, "primary", url)] + end + end + + def method_missing(method, *args, &blk) + if Hash.method_defined?(method) + ActiveSupport::Deprecation.warn \ + "Returning a hash from ActiveRecord::Base.configurations is deprecated. Therefore calling `#{method}` on the hash is also deprecated. Please switch to using the `configs_for` method instead to collect and iterate over database configurations." + end + + case method + when :each, :first + configurations.send(method, *args, &blk) + when :fetch + configs_for(env_name: args.first) + when :values + configurations.map(&:config) + else + super + end end - end end end diff --git a/activerecord/lib/active_record/database_configurations/database_config.rb b/activerecord/lib/active_record/database_configurations/database_config.rb new file mode 100644 index 0000000000..6250827b34 --- /dev/null +++ b/activerecord/lib/active_record/database_configurations/database_config.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module ActiveRecord + class DatabaseConfigurations + # ActiveRecord::Base.configurations will return either a HashConfig or + # UrlConfig respectively. It will never return a DatabaseConfig object, + # as this is the parent class for the types of database configuration objects. + class DatabaseConfig # :nodoc: + attr_reader :env_name, :spec_name + + def initialize(env_name, spec_name) + @env_name = env_name + @spec_name = spec_name + end + + def replica? + raise NotImplementedError + end + + def url_config? + false + end + + def to_legacy_hash + { env_name => config } + end + + def for_current_env? + env_name == ActiveRecord::ConnectionHandling::DEFAULT_ENV.call + end + end + end +end diff --git a/activerecord/lib/active_record/database_configurations/hash_config.rb b/activerecord/lib/active_record/database_configurations/hash_config.rb new file mode 100644 index 0000000000..18ed7c0466 --- /dev/null +++ b/activerecord/lib/active_record/database_configurations/hash_config.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module ActiveRecord + class DatabaseConfigurations + # A HashConfig object is created for each database configuration entry that + # is created from a hash. + # + # A hash config: + # + # { "development" => { "database" => "db_name" } } + # + # Becomes: + # + # #<ActiveRecord::DatabaseConfigurations::HashConfig:0x00007fd1acbded10 + # @env_name="development", @spec_name="primary", @config={"db_name"}> + # + # Options are: + # + # <tt>:env_name</tt> - The Rails environment, ie "development" + # <tt>:spec_name</tt> - The specification name. In a standard two-tier + # database configuration this will default to "primary". In a multiple + # database three-tier database configuration this corresponds to the name + # used in the second tier, for example "primary_readonly". + # <tt>:config</tt> - The config hash. This is the hash that contains the + # database adapter, name, and other important information for database + # connections. + class HashConfig < DatabaseConfig + attr_reader :config + + def initialize(env_name, spec_name, config) + super(env_name, spec_name) + @config = config + end + + # Determines whether a database configuration is for a replica / readonly + # connection. If the `replica` key is present in the config, `replica?` will + # return +true+. + def replica? + config["replica"] + end + end + end +end diff --git a/activerecord/lib/active_record/database_configurations/url_config.rb b/activerecord/lib/active_record/database_configurations/url_config.rb new file mode 100644 index 0000000000..f526c59d56 --- /dev/null +++ b/activerecord/lib/active_record/database_configurations/url_config.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +module ActiveRecord + class DatabaseConfigurations + # A UrlConfig object is created for each database configuration + # entry that is created from a URL. This can either be a URL string + # or a hash with a URL in place of the config hash. + # + # A URL config: + # + # postgres://localhost/foo + # + # Becomes: + # + # #<ActiveRecord::DatabaseConfigurations::UrlConfig:0x00007fdc3238f340 + # @env_name="default_env", @spec_name="primary", + # @config={"adapter"=>"postgresql", "database"=>"foo", "host"=>"localhost"}, + # @url="postgres://localhost/foo"> + # + # Options are: + # + # <tt>:env_name</tt> - The Rails environment, ie "development" + # <tt>:spec_name</tt> - The specification name. In a standard two-tier + # database configuration this will default to "primary". In a multiple + # database three-tier database configuration this corresponds to the name + # used in the second tier, for example "primary_readonly". + # <tt>:url</tt> - The database URL. + # <tt>:config</tt> - The config hash. This is the hash that contains the + # database adapter, name, and other important information for database + # connections. + class UrlConfig < DatabaseConfig + attr_reader :url, :config + + def initialize(env_name, spec_name, url, config = {}) + super(env_name, spec_name) + @config = build_config(config, url) + @url = url + end + + def url_config? # :nodoc: + true + end + + # Determines whether a database configuration is for a replica / readonly + # connection. If the `replica` key is present in the config, `replica?` will + # return +true+. + def replica? + config["replica"] + end + + private + def build_config(original_config, url) + if /^jdbc:/.match?(url) + hash = { "url" => url } + else + hash = ActiveRecord::ConnectionAdapters::ConnectionSpecification::ConnectionUrlResolver.new(url).to_hash + end + + if original_config[env_name] + original_config[env_name].merge(hash) + else + original_config.merge(hash) + end + end + end + end +end diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index 1030b2368b..4a3a31fc95 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -61,7 +61,7 @@ module ActiveRecord end private - def _create_record(attribute_names = self.attribute_names, *) + def _create_record(attribute_names = self.attribute_names) if locking_enabled? # We always want to persist the locking version, even if we don't detect # a change from the default, since the database might have no default diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb index 1ae6840921..6b84431343 100644 --- a/activerecord/lib/active_record/log_subscriber.rb +++ b/activerecord/lib/active_record/log_subscriber.rb @@ -4,6 +4,8 @@ module ActiveRecord class LogSubscriber < ActiveSupport::LogSubscriber IGNORE_PAYLOAD_NAMES = ["SCHEMA", "EXPLAIN"] + class_attribute :backtrace_cleaner, default: ActiveSupport::BacktraceCleaner.new + def self.runtime=(value) ActiveRecord::RuntimeRegistry.sql_runtime = value end @@ -100,21 +102,15 @@ module ActiveRecord end def log_query_source - location = extract_query_source_location(caller_locations) - - if location - source = "#{location.path}:#{location.lineno}" - source = source.sub("#{::Rails.root}/", "") if defined?(::Rails.root) + source = extract_query_source_location(caller) + if source logger.debug(" ↳ #{source}") end end - RAILS_GEM_ROOT = File.expand_path("../../..", __dir__) + "/" - PATHS_TO_IGNORE = /\A(#{RAILS_GEM_ROOT}|#{RbConfig::CONFIG["rubylibdir"]})/ - def extract_query_source_location(locations) - locations.find { |line| line.absolute_path && !line.absolute_path.match?(PATHS_TO_IGNORE) } + backtrace_cleaner.clean(locations).first end end end diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb index 087632b10f..dea6d4ec08 100644 --- a/activerecord/lib/active_record/migration/command_recorder.rb +++ b/activerecord/lib/active_record/migration/command_recorder.rb @@ -214,11 +214,24 @@ module ActiveRecord end def invert_remove_foreign_key(args) - from_table, to_table, remove_options = args - raise ActiveRecord::IrreversibleMigration, "remove_foreign_key is only reversible if given a second table" if to_table.nil? || to_table.is_a?(Hash) + from_table, options_or_to_table, options_or_nil = args + + to_table = if options_or_to_table.is_a?(Hash) + options_or_to_table[:to_table] + else + options_or_to_table + end + + remove_options = if options_or_to_table.is_a?(Hash) + options_or_to_table.except(:to_table) + else + options_or_nil + end + + raise ActiveRecord::IrreversibleMigration, "remove_foreign_key is only reversible if given a second table" if to_table.nil? reversed_args = [from_table, to_table] - reversed_args << remove_options if remove_options + reversed_args << remove_options if remove_options.present? [:add_foreign_key, reversed_args] end diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 05963e5546..24af56cf0b 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -718,7 +718,6 @@ module ActiveRecord # Updates the associated record with values matching those of the instance attributes. # Returns the number of affected rows. def _update_record(attribute_names = self.attribute_names) - attribute_names &= self.class.column_names attribute_names = attributes_for_update(attribute_names) if attribute_names.empty? @@ -737,10 +736,12 @@ module ActiveRecord # Creates a record with values matching those of the instance attributes # and returns its id. def _create_record(attribute_names = self.attribute_names) - attribute_names &= self.class.column_names - attributes_values = attributes_with_values_for_create(attribute_names) + attribute_names = attributes_for_create(attribute_names) + + new_id = self.class._insert_record( + attributes_with_values(attribute_names) + ) - new_id = self.class._insert_record(attributes_values) self.id ||= new_id if self.class.primary_key @new_record = false diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index 009f412234..47351588d3 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -77,6 +77,10 @@ module ActiveRecord ActiveSupport.on_load(:active_record) { self.logger ||= ::Rails.logger } end + initializer "active_record.backtrace_cleaner" do + ActiveSupport.on_load(:active_record) { LogSubscriber.backtrace_cleaner = ::Rails.backtrace_cleaner } + end + initializer "active_record.migration_error" do if config.active_record.delete(:migration_error) == :page_load config.app_middleware.insert_after ::ActionDispatch::Callbacks, @@ -231,5 +235,11 @@ MSG end end end + + initializer "active_record.set_filter_attributes" do + ActiveSupport.on_load(:active_record) do + self.filter_attributes += Rails.application.config.filter_parameters + end + end end end diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 8b7d18fb3d..be5ef350a9 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -26,7 +26,7 @@ db_namespace = namespace :db do ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name| desc "Create #{spec_name} database for current environment" task spec_name => :load_config do - db_config = ActiveRecord::DatabaseConfigurations.config_for_env_and_spec(Rails.env, spec_name) + db_config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, spec_name: spec_name) ActiveRecord::Tasks::DatabaseTasks.create(db_config.config) end end @@ -45,7 +45,7 @@ db_namespace = namespace :db do ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name| desc "Drop #{spec_name} database for current environment" task spec_name => [:load_config, :check_protected_environments] do - db_config = ActiveRecord::DatabaseConfigurations.config_for_env_and_spec(Rails.env, spec_name) + db_config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, spec_name: spec_name) ActiveRecord::Tasks::DatabaseTasks.drop(db_config.config) end end @@ -73,8 +73,8 @@ db_namespace = namespace :db do desc "Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)." task migrate: :load_config do - ActiveRecord::DatabaseConfigurations.configs_for(Rails.env) do |spec_name, config| - ActiveRecord::Base.establish_connection(config) + ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config| + ActiveRecord::Base.establish_connection(db_config.config) ActiveRecord::Tasks::DatabaseTasks.migrate end db_namespace["_dump"].invoke @@ -99,7 +99,7 @@ db_namespace = namespace :db do ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name| desc "Migrate #{spec_name} database for current environment" task spec_name => :load_config do - db_config = ActiveRecord::DatabaseConfigurations.config_for_env_and_spec(Rails.env, spec_name) + db_config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, spec_name: spec_name) ActiveRecord::Base.establish_connection(db_config.config) ActiveRecord::Tasks::DatabaseTasks.migrate end @@ -274,11 +274,10 @@ db_namespace = namespace :db do desc "Creates a db/schema.rb file that is portable against any DB supported by Active Record" task dump: :load_config do require "active_record/schema_dumper" - - ActiveRecord::DatabaseConfigurations.configs_for(Rails.env) do |spec_name, config| - filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(spec_name, :ruby) + ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config| + filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(db_config.spec_name, :ruby) File.open(filename, "w:utf-8") do |file| - ActiveRecord::Base.establish_connection(config) + ActiveRecord::Base.establish_connection(db_config.config) ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file) end end @@ -314,11 +313,10 @@ db_namespace = namespace :db do namespace :structure do desc "Dumps the database structure to db/structure.sql. Specify another file with SCHEMA=db/my_structure.sql" task dump: :load_config do - ActiveRecord::DatabaseConfigurations.configs_for(Rails.env) do |spec_name, config| - ActiveRecord::Base.establish_connection(config) - filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(spec_name, :sql) - ActiveRecord::Tasks::DatabaseTasks.structure_dump(config, filename) - + ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config| + ActiveRecord::Base.establish_connection(db_config.config) + filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(db_config.spec_name, :sql) + ActiveRecord::Tasks::DatabaseTasks.structure_dump(db_config.config, filename) if ActiveRecord::SchemaMigration.table_exists? File.open(filename, "a") do |f| f.puts ActiveRecord::Base.connection.dump_schema_information @@ -356,22 +354,30 @@ db_namespace = namespace :db do begin should_reconnect = ActiveRecord::Base.connection_pool.active_connection? ActiveRecord::Schema.verbose = false - ActiveRecord::Tasks::DatabaseTasks.load_schema ActiveRecord::Base.configurations["test"], :ruby, ENV["SCHEMA"], "test" + ActiveRecord::Base.configurations.configs_for(env_name: "test").each do |db_config| + filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(db_config.spec_name, :ruby) + ActiveRecord::Tasks::DatabaseTasks.load_schema(db_config.config, :ruby, filename, "test") + end ensure if should_reconnect - ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[ActiveRecord::Tasks::DatabaseTasks.env]) + ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations.default_hash(ActiveRecord::Tasks::DatabaseTasks.env)) end end end # desc "Recreate the test database from an existent structure.sql file" task load_structure: %w(db:test:purge) do - ActiveRecord::Tasks::DatabaseTasks.load_schema ActiveRecord::Base.configurations["test"], :sql, ENV["SCHEMA"], "test" + ActiveRecord::Base.configurations.configs_for(env_name: "test").each do |db_config| + filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(db_config.spec_name, :sql) + ActiveRecord::Tasks::DatabaseTasks.load_schema(db_config.config, :sql, filename, "test") + end end # desc "Empty the test database" task purge: %w(load_config check_protected_environments) do - ActiveRecord::Tasks::DatabaseTasks.purge ActiveRecord::Base.configurations["test"] + ActiveRecord::Base.configurations.configs_for(env_name: "test").each do |db_config| + ActiveRecord::Tasks::DatabaseTasks.purge(db_config.config) + end end # desc 'Load the test schema' diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 6d2f75a3ae..b2110f727c 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -612,9 +612,21 @@ module ActiveRecord # returns either +nil+ or the inverse association name that it finds. def automatic_inverse_of - if can_find_inverse_of_automatically?(self) - inverse_name = ActiveSupport::Inflector.underscore(options[:as] || active_record.name.demodulize).to_sym + return unless can_find_inverse_of_automatically?(self) + inverse_name_candidates = + if options[:as] + [options[:as]] + else + active_record_name = active_record.name.demodulize + [active_record_name, ActiveSupport::Inflector.pluralize(active_record_name)] + end + + inverse_name_candidates.map! do |candidate| + ActiveSupport::Inflector.underscore(candidate).to_sym + end + + inverse_name_candidates.detect do |inverse_name| begin reflection = klass._reflect_on_association(inverse_name) rescue NameError @@ -623,9 +635,7 @@ module ActiveRecord reflection = false end - if valid_inverse_reflection?(reflection) - return inverse_name - end + valid_inverse_reflection?(reflection) end end diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 2d3e1eaa08..29a3ceab7d 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -375,8 +375,12 @@ module ActiveRecord @klass.connection.update stmt, "#{@klass} Update All" end - def update(attributes) # :nodoc: - each { |record| record.update(attributes) } + def update(id = :all, attributes) # :nodoc: + if id == :all + each { |record| record.update(attributes) } + else + klass.update(id, attributes) + end end def update_counters(counters) # :nodoc: diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 40fe39fa9d..0fa5ba2e50 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -190,7 +190,7 @@ module ActiveRecord relation = apply_join_dependency relation.pluck(*column_names) else - enforce_raw_sql_whitelist(column_names) + disallow_raw_sql!(column_names) relation = spawn relation.select_values = column_names.map { |cn| @klass.has_attribute?(cn) || @klass.attribute_alias?(cn) ? arel_attribute(cn) : cn diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb index 10e4779ca4..07b16a0740 100644 --- a/activerecord/lib/active_record/relation/merger.rb +++ b/activerecord/lib/active_record/relation/merger.rb @@ -152,10 +152,10 @@ module ActiveRecord def merge_multi_values if other.reordering_value # override any order specified in the original relation - relation.reorder! other.order_values + relation.reorder!(*other.order_values) elsif other.order_values.any? # merge in order_values from relation - relation.order! other.order_values + relation.order!(*other.order_values) end extensions = other.extensions - relation.extensions diff --git a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb index 64bf83e3c1..e5191fa38a 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "active_support/core_ext/array/extract" + module ActiveRecord class PredicateBuilder class ArrayHandler # :nodoc: @@ -11,8 +13,8 @@ module ActiveRecord return attribute.in([]) if value.empty? values = value.map { |x| x.is_a?(Base) ? x.id : x } - nils, values = values.partition(&:nil?) - ranges, values = values.partition { |v| v.is_a?(Range) } + nils = values.extract!(&:nil?) + ranges = values.extract! { |v| v.is_a?(Range) } values_predicate = case values.length diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 52405f21a1..56497e11cb 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -1133,9 +1133,9 @@ module ActiveRecord end order_args.flatten! - @klass.enforce_raw_sql_whitelist( + @klass.disallow_raw_sql!( order_args.flat_map { |a| a.is_a?(Hash) ? a.keys : a }, - whitelist: AttributeMethods::ClassMethods::COLUMN_NAME_ORDER_WHITELIST + permit: AttributeMethods::ClassMethods::COLUMN_NAME_WITH_ORDER ) validate_order_args(order_args) diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb index 7f1c2fd7eb..3b2556b1c8 100644 --- a/activerecord/lib/active_record/result.rb +++ b/activerecord/lib/active_record/result.rb @@ -140,6 +140,8 @@ module ActiveRecord # We freeze the strings to prevent them getting duped when # used as keys in ActiveRecord::Base's @attributes hash columns = @columns.map(&:-@) + length = columns.length + @rows.map { |row| # In the past we used Hash[columns.zip(row)] # though elegant, the verbose way is much more efficient @@ -148,8 +150,6 @@ module ActiveRecord hash = {} index = 0 - length = columns.length - while index < length hash[columns[index]] = row[index] index += 1 diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index c6c268855e..3485d9e557 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -61,8 +61,8 @@ module ActiveRecord # # => "id ASC" def sanitize_sql_for_order(condition) if condition.is_a?(Array) && condition.first.to_s.include?("?") - enforce_raw_sql_whitelist([condition.first], - whitelist: AttributeMethods::ClassMethods::COLUMN_NAME_ORDER_WHITELIST + disallow_raw_sql!([condition.first], + permit: AttributeMethods::ClassMethods::COLUMN_NAME_WITH_ORDER ) # Ensure we aren't dealing with a subclass of String that might diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index fd36c0abd2..16e66982e5 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "active_record/database_configurations" + module ActiveRecord module Tasks # :nodoc: class DatabaseAlreadyExists < StandardError; end # :nodoc: @@ -101,16 +103,21 @@ module ActiveRecord @env ||= Rails.env end + def spec + @spec ||= "primary" + end + def seed_loader @seed_loader ||= Rails.application end def current_config(options = {}) options.reverse_merge! env: env + options[:spec] ||= "primary" if options.has_key?(:config) @current_config = options[:config] else - @current_config ||= ActiveRecord::Base.configurations[options[:env]] + @current_config ||= ActiveRecord::Base.configurations.configs_for(env_name: options[:env], spec_name: options[:spec]).config end end @@ -122,7 +129,7 @@ module ActiveRecord $stderr.puts "Database '#{configuration['database']}' already exists" if verbose? rescue Exception => error $stderr.puts error - $stderr.puts "Couldn't create database for #{configuration.inspect}" + $stderr.puts "Couldn't create '#{configuration['database']}' database. Please check your configuration." raise end @@ -135,8 +142,8 @@ module ActiveRecord end def for_each - databases = Rails.application.config.load_database_yaml - database_configs = ActiveRecord::DatabaseConfigurations.configs_for(Rails.env, databases) + databases = Rails.application.config.database_configuration + database_configs = ActiveRecord::DatabaseConfigurations.new(databases).configs_for(env_name: Rails.env) # if this is a single database application we don't want tasks for each primary database return if database_configs.count == 1 @@ -180,9 +187,11 @@ module ActiveRecord scope = ENV["SCOPE"] verbose_was, Migration.verbose = Migration.verbose, verbose? + Base.connection.migration_context.migrate(target_version) do |migration| scope.blank? || scope == migration.scope end + ActiveRecord::Base.clear_cache! ensure Migration.verbose = verbose_was @@ -198,8 +207,8 @@ module ActiveRecord ENV["VERSION"].to_i if ENV["VERSION"] && !ENV["VERSION"].empty? end - def charset_current(environment = env) - charset ActiveRecord::Base.configurations[environment] + def charset_current(environment = env, specification_name = spec) + charset ActiveRecord::Base.configurations.configs_for(env_name: environment, spec_name: specification_name).config end def charset(*arguments) @@ -207,8 +216,8 @@ module ActiveRecord class_for_adapter(configuration["adapter"]).new(*arguments).charset end - def collation_current(environment = env) - collation ActiveRecord::Base.configurations[environment] + def collation_current(environment = env, specification_name = spec) + collation ActiveRecord::Base.configurations.configs_for(env_name: environment, spec_name: specification_name).config end def collation(*arguments) @@ -342,14 +351,15 @@ module ActiveRecord environments << "test" if environment == "development" environments.each do |env| - ActiveRecord::DatabaseConfigurations.configs_for(env) do |spec_name, configuration| - yield configuration, spec_name, env + ActiveRecord::Base.configurations.configs_for(env_name: env).each do |db_config| + yield db_config.config, db_config.spec_name, env end end end def each_local_configuration - ActiveRecord::Base.configurations.each_value do |configuration| + ActiveRecord::Base.configurations.configs_for.each do |db_config| + configuration = db_config.config next unless configuration["database"] if local_database?(configuration) diff --git a/activerecord/lib/active_record/test_databases.rb b/activerecord/lib/active_record/test_databases.rb index 5b4efe22c9..999830ba79 100644 --- a/activerecord/lib/active_record/test_databases.rb +++ b/activerecord/lib/active_record/test_databases.rb @@ -5,31 +5,32 @@ require "active_support/testing/parallelization" module ActiveRecord module TestDatabases # :nodoc: ActiveSupport::Testing::Parallelization.after_fork_hook do |i| - create_and_load_schema(i, spec_name: Rails.env) + create_and_load_schema(i, env_name: Rails.env) end - ActiveSupport::Testing::Parallelization.run_cleanup_hook do |_| - drop(spec_name: Rails.env) + ActiveSupport::Testing::Parallelization.run_cleanup_hook do + drop(env_name: Rails.env) end - def self.create_and_load_schema(i, spec_name:) + def self.create_and_load_schema(i, env_name:) old, ENV["VERBOSE"] = ENV["VERBOSE"], "false" - connection_spec = ActiveRecord::Base.configurations[spec_name] - - connection_spec["database"] += "-#{i}" - ActiveRecord::Tasks::DatabaseTasks.create(connection_spec) - ActiveRecord::Tasks::DatabaseTasks.load_schema(connection_spec) + ActiveRecord::Base.configurations.configs_for(env_name: env_name).each do |db_config| + db_config.config["database"] += "-#{i}" + ActiveRecord::Tasks::DatabaseTasks.create(db_config.config) + ActiveRecord::Tasks::DatabaseTasks.load_schema(db_config.config, ActiveRecord::Base.schema_format, nil, env_name, db_config.spec_name) + end ensure - ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[Rails.env]) + ActiveRecord::Base.establish_connection(Rails.env.to_sym) ENV["VERBOSE"] = old end - def self.drop(spec_name:) + def self.drop(env_name:) old, ENV["VERBOSE"] = ENV["VERBOSE"], "false" - connection_spec = ActiveRecord::Base.configurations[spec_name] - ActiveRecord::Tasks::DatabaseTasks.drop(connection_spec) + ActiveRecord::Base.configurations.configs_for(env_name: env_name).each do |db_config| + ActiveRecord::Tasks::DatabaseTasks.drop(db_config.config) + end ensure ENV["VERBOSE"] = old end diff --git a/activerecord/lib/rails/generators/active_record/migration.rb b/activerecord/lib/rails/generators/active_record/migration.rb index 4ceb502c5d..4a17082d66 100644 --- a/activerecord/lib/rails/generators/active_record/migration.rb +++ b/activerecord/lib/rails/generators/active_record/migration.rb @@ -24,7 +24,9 @@ module ActiveRecord end def db_migrate_path - if defined?(Rails.application) && Rails.application + if migrations_paths = options[:migrations_paths] + migrations_paths + elsif defined?(Rails.application) && Rails.application Rails.application.config.paths["db/migrate"].to_ary.first else "db/migrate" diff --git a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb index a07b00ef79..281b7afb50 100644 --- a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb +++ b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb @@ -8,6 +8,7 @@ module ActiveRecord argument :attributes, type: :array, default: [], banner: "field[:type][:index] field[:type][:index]" class_option :primary_key_type, type: :string, desc: "The type for primary key" + class_option :migrations_paths, type: :string, desc: "The migration path for your generated migrations. If this is not set it will default to db/migrate" def create_migration_file set_local_assigns! diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb index 59b99351d1..1c461a0459 100644 --- a/activerecord/test/cases/adapter_test.rb +++ b/activerecord/test/cases/adapter_test.rb @@ -10,6 +10,7 @@ module ActiveRecord class AdapterTest < ActiveRecord::TestCase def setup @connection = ActiveRecord::Base.connection + @connection.materialize_transactions end ## @@ -299,6 +300,34 @@ module ActiveRecord def test_supports_multi_insert_is_deprecated assert_deprecated { @connection.supports_multi_insert? } end + + def test_column_name_length_is_deprecated + assert_deprecated { @connection.column_name_length } + end + + def test_table_name_length_is_deprecated + assert_deprecated { @connection.table_name_length } + end + + def test_columns_per_table_is_deprecated + assert_deprecated { @connection.columns_per_table } + end + + def test_indexes_per_table_is_deprecated + assert_deprecated { @connection.indexes_per_table } + end + + def test_columns_per_multicolumn_index_is_deprecated + assert_deprecated { @connection.columns_per_multicolumn_index } + end + + def test_sql_query_length_is_deprecated + assert_deprecated { @connection.sql_query_length } + end + + def test_joins_per_query_is_deprecated + assert_deprecated { @connection.joins_per_query } + end end class AdapterForeignKeyTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/adapters/helpers/test_supports_advisory_locks.rb b/activerecord/test/cases/adapters/helpers/test_supports_advisory_locks.rb new file mode 100644 index 0000000000..4905e17725 --- /dev/null +++ b/activerecord/test/cases/adapters/helpers/test_supports_advisory_locks.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require "support/connection_helper" + +module TestSupportsAdvisoryLocks + include ConnectionHelper + + def test_supports_advisory_locks? + assert ActiveRecord::Base.connection.supports_advisory_locks? + + run_without_connection do |orig_connection| + ActiveRecord::Base.establish_connection( + orig_connection.merge(advisory_locks: false) + ) + + assert_not ActiveRecord::Base.connection.supports_advisory_locks? + + ActiveRecord::Base.establish_connection( + orig_connection.merge(advisory_locks: true) + ) + + assert ActiveRecord::Base.connection.supports_advisory_locks? + end + end +end diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb index 726f58d58e..0c0e2a116e 100644 --- a/activerecord/test/cases/adapters/mysql2/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb @@ -170,6 +170,8 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase end def test_logs_name_show_variable + ActiveRecord::Base.connection.materialize_transactions + @subscriber.logged.clear @connection.show_variable "foo" assert_equal "SCHEMA", @subscriber.logged[0][1] end diff --git a/activerecord/test/cases/adapters/mysql2/test_advisory_locks_disabled_test.rb b/activerecord/test/cases/adapters/mysql2/test_advisory_locks_disabled_test.rb new file mode 100644 index 0000000000..4857900820 --- /dev/null +++ b/activerecord/test/cases/adapters/mysql2/test_advisory_locks_disabled_test.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +require "cases/helper" +require "cases/adapters/helpers/test_supports_advisory_locks" + +class Mysql2AdvisoryLocksDisabledTest < ActiveRecord::Mysql2TestCase + include TestSupportsAdvisoryLocks +end diff --git a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb index 308ad1d854..afd422881b 100644 --- a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb @@ -4,6 +4,8 @@ require "cases/helper" class PostgresqlActiveSchemaTest < ActiveRecord::PostgreSQLTestCase def setup + ActiveRecord::Base.connection.materialize_transactions + ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.class_eval do def execute(sql, name = nil) sql end end diff --git a/activerecord/test/cases/adapters/postgresql/advisory_locks_disabled_test.rb b/activerecord/test/cases/adapters/postgresql/advisory_locks_disabled_test.rb new file mode 100644 index 0000000000..f14e9baeb9 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/advisory_locks_disabled_test.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +require "cases/helper" +require "cases/adapters/helpers/test_supports_advisory_locks" + +class PostgresqlAdvisoryLocksDisabledTest < ActiveRecord::PostgreSQLTestCase + include TestSupportsAdvisoryLocks +end diff --git a/activerecord/test/cases/adapters/postgresql/connection_test.rb b/activerecord/test/cases/adapters/postgresql/connection_test.rb index 54b0dde7dc..70aa189893 100644 --- a/activerecord/test/cases/adapters/postgresql/connection_test.rb +++ b/activerecord/test/cases/adapters/postgresql/connection_test.rb @@ -15,8 +15,9 @@ module ActiveRecord def setup super @subscriber = SQLSubscriber.new - @subscription = ActiveSupport::Notifications.subscribe("sql.active_record", @subscriber) @connection = ActiveRecord::Base.connection + @connection.materialize_transactions + @subscription = ActiveSupport::Notifications.subscribe("sql.active_record", @subscriber) end def teardown diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index 5b8d4722af..a1fba8dc66 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -1571,8 +1571,9 @@ class EagerAssociationTest < ActiveRecord::TestCase # CollectionProxy#reader is expensive, so the preloader avoids calling it. test "preloading has_many_through association avoids calling association.reader" do - ActiveRecord::Associations::HasManyAssociation.any_instance.expects(:reader).never - Author.preload(:readonly_comments).first! + assert_not_called_on_instance_of(ActiveRecord::Associations::HasManyAssociation, :reader) do + Author.preload(:readonly_comments).first! + end end test "preloading through a polymorphic association doesn't require the association to exist" do diff --git a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb index f414fbf64b..482302055d 100644 --- a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb @@ -781,7 +781,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase end def test_has_many_through_polymorphic_has_manys_works - assert_equal [10, 20].to_set, pirates(:redbeard).treasure_estimates.map(&:price).to_set + assert_equal ["$10.00", "$20.00"].to_set, pirates(:redbeard).treasure_estimates.map(&:price).to_set end def test_symbols_as_keys diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 0ca902385a..a2f6174dc1 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -27,7 +27,6 @@ require "models/categorization" require "models/minivan" require "models/speedometer" require "models/reference" -require "models/job" require "models/college" require "models/student" require "models/pirate" @@ -114,7 +113,7 @@ end class HasManyAssociationsTest < ActiveRecord::TestCase fixtures :accounts, :categories, :companies, :developers, :projects, :developers_projects, :topics, :authors, :author_addresses, :comments, - :posts, :readers, :taggings, :cars, :jobs, :tags, + :posts, :readers, :taggings, :cars, :tags, :categorizations, :zines, :interests def setup @@ -377,6 +376,27 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal invoice.id, line_item.invoice_id end + class SpecialAuthor < ActiveRecord::Base + self.table_name = "authors" + has_many :books, class_name: "SpecialBook", foreign_key: :author_id + end + + class SpecialBook < ActiveRecord::Base + self.table_name = "books" + + belongs_to :author + enum read_status: { unread: 0, reading: 2, read: 3, forgotten: nil } + end + + def test_association_enum_works_properly + author = SpecialAuthor.create!(name: "Test") + book = SpecialBook.create!(read_status: "reading") + author.books << book + + assert_equal "reading", book.read_status + assert_not_equal 0, SpecialAuthor.joins(:books).where(books: { read_status: "reading" }).count + end + # When creating objects on the association, we must not do it within a scope (even though it # would be convenient), because this would cause that scope to be applied to any callbacks etc. def test_build_and_create_should_not_happen_within_scope @@ -2134,21 +2154,29 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_defining_has_many_association_with_delete_all_dependency_lazily_evaluates_target_class - ActiveRecord::Reflection::AssociationReflection.any_instance.expects(:class_name).never - class_eval(<<-EOF, __FILE__, __LINE__ + 1) - class DeleteAllModel < ActiveRecord::Base - has_many :nonentities, :dependent => :delete_all - end - EOF + assert_not_called_on_instance_of( + ActiveRecord::Reflection::AssociationReflection, + :class_name, + ) do + class_eval(<<-EOF, __FILE__, __LINE__ + 1) + class DeleteAllModel < ActiveRecord::Base + has_many :nonentities, :dependent => :delete_all + end + EOF + end end def test_defining_has_many_association_with_nullify_dependency_lazily_evaluates_target_class - ActiveRecord::Reflection::AssociationReflection.any_instance.expects(:class_name).never - class_eval(<<-EOF, __FILE__, __LINE__ + 1) - class NullifyModel < ActiveRecord::Base - has_many :nonentities, :dependent => :nullify - end - EOF + assert_not_called_on_instance_of( + ActiveRecord::Reflection::AssociationReflection, + :class_name, + ) do + class_eval(<<-EOF, __FILE__, __LINE__ + 1) + class NullifyModel < ActiveRecord::Base + has_many :nonentities, :dependent => :nullify + end + EOF + end end def test_attributes_are_being_set_when_initialized_from_has_many_association_with_where_clause diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index d7e898a1c0..9eea34d2b9 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -661,6 +661,8 @@ class HasOneAssociationsTest < ActiveRecord::TestCase self.table_name = "books" belongs_to :author, class_name: "SpecialAuthor" has_one :subscription, class_name: "SpecialSupscription", foreign_key: "subscriber_id" + + enum status: [:proposed, :written, :published] end class SpecialAuthor < ActiveRecord::Base @@ -678,6 +680,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase book = SpecialBook.create!(status: "published") author.book = book + assert_equal "published", book.status assert_not_equal 0, SpecialAuthor.joins(:book).where(books: { status: "published" }).count end diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb index da3a42e2b5..eb4dc73423 100644 --- a/activerecord/test/cases/associations/inverse_associations_test.rb +++ b/activerecord/test/cases/associations/inverse_associations_test.rb @@ -20,6 +20,8 @@ require "models/company" require "models/project" require "models/author" require "models/post" +require "models/department" +require "models/hotel" class AutomaticInverseFindingTests < ActiveRecord::TestCase fixtures :ratings, :comments, :cars @@ -724,6 +726,16 @@ class InversePolymorphicBelongsToTests < ActiveRecord::TestCase # fails because Interest does have the correct inverse_of assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Face.first.polymorphic_man = Interest.first } end + + def test_favors_has_one_associations_for_inverse_of + inverse_name = Post.reflect_on_association(:author).inverse_of.name + assert_equal :post, inverse_name + end + + def test_finds_inverse_of_for_plural_associations + inverse_name = Department.reflect_on_association(:hotel).inverse_of.name + assert_equal :departments, inverse_name + end end # NOTE - these tests might not be meaningful, ripped as they were from the parental_control plugin diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index ade1f4b44d..fa618735d7 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -558,6 +558,13 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa assert_equal no_of_clients + 1, Client.count end + def test_parent_should_save_children_record_with_foreign_key_validation_set_in_before_save_callback + company = NewlyContractedCompany.new(name: "test") + + assert company.save + assert_not_empty company.reload.new_contracts + end + def test_parent_should_not_get_saved_with_duplicate_children_records assert_no_difference "Reply.count" do assert_no_difference "SillyUniqueReply.count" do @@ -568,7 +575,13 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa ]) assert_not reply.save - assert_not_empty reply.errors + assert_equal ["is invalid"], reply.errors[:silly_unique_replies] + assert_empty reply.silly_unique_replies.first.errors + + assert_equal( + ["has already been taken"], + reply.silly_unique_replies.last.errors[:content] + ) end end end diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index d216fe16fa..f6311f9256 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -853,8 +853,7 @@ class BasicsTest < ActiveRecord::TestCase assert_equal company, Company.find(company.id) end - # TODO: extend defaults tests to other databases! - if current_adapter?(:PostgreSQLAdapter) + if current_adapter?(:PostgreSQLAdapter, :Mysql2Adapter, :SQLite3Adapter) def test_default with_timezone_config default: :local do default = Default.new @@ -866,7 +865,10 @@ class BasicsTest < ActiveRecord::TestCase # char types assert_equal "Y", default.char1 assert_equal "a varchar field", default.char2 - assert_equal "a text field", default.char3 + # Mysql text type can't have default value + unless current_adapter?(:Mysql2Adapter) + assert_equal "a text field", default.char3 + end end end end diff --git a/activerecord/test/cases/connection_adapters/connection_handler_test.rb b/activerecord/test/cases/connection_adapters/connection_handler_test.rb index b8e623f17b..8c204a2692 100644 --- a/activerecord/test/cases/connection_adapters/connection_handler_test.rb +++ b/activerecord/test/cases/connection_adapters/connection_handler_test.rb @@ -28,13 +28,16 @@ module ActiveRecord end def test_establish_connection_uses_spec_name + old_config = ActiveRecord::Base.configurations config = { "readonly" => { "adapter" => "sqlite3" } } - resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(config) + ActiveRecord::Base.configurations = config + resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(ActiveRecord::Base.configurations) spec = resolver.spec(:readonly) @handler.establish_connection(spec.to_hash) assert_not_nil @handler.retrieve_connection_pool("readonly") ensure + ActiveRecord::Base.configurations = old_config @handler.remove_connection("readonly") end diff --git a/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb b/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb index 1b64324cc4..06c1c51724 100644 --- a/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb +++ b/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb @@ -18,11 +18,14 @@ module ActiveRecord end def resolve_config(config) - ActiveRecord::ConnectionHandling::MergeAndResolveDefaultUrlConfig.new(config).resolve + configs = ActiveRecord::DatabaseConfigurations.new(config) + configs.to_h end def resolve_spec(spec, config) - ConnectionSpecification::Resolver.new(resolve_config(config)).resolve(spec) + configs = ActiveRecord::DatabaseConfigurations.new(config) + resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(configs) + resolver.resolve(spec, spec) end def test_resolver_with_database_uri_and_current_env_symbol_key diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb index 9ac03629c3..06869eeab0 100644 --- a/activerecord/test/cases/connection_pool_test.rb +++ b/activerecord/test/cases/connection_pool_test.rb @@ -91,7 +91,9 @@ module ActiveRecord end def test_full_pool_exception + @pool.checkout_timeout = 0.001 # no need to delay test suite by waiting the whole full default timeout @pool.size.times { assert @pool.checkout } + assert_raises(ConnectionTimeoutError) do @pool.checkout end @@ -109,6 +111,44 @@ module ActiveRecord assert_equal connection, t.join.value end + def test_full_pool_blocking_shares_load_interlock + @pool.instance_variable_set(:@size, 1) + + load_interlock_latch = Concurrent::CountDownLatch.new + connection_latch = Concurrent::CountDownLatch.new + + able_to_get_connection = false + able_to_load = false + + thread_with_load_interlock = Thread.new do + ActiveSupport::Dependencies.interlock.running do + load_interlock_latch.count_down + connection_latch.wait + + @pool.with_connection do + able_to_get_connection = true + end + end + end + + thread_with_last_connection = Thread.new do + @pool.with_connection do + connection_latch.count_down + load_interlock_latch.wait + + ActiveSupport::Dependencies.interlock.loading do + able_to_load = true + end + end + end + + thread_with_load_interlock.join + thread_with_last_connection.join + + assert able_to_get_connection + assert able_to_load + end + def test_removing_releases_latch cs = @pool.size.times.map { @pool.checkout } t = Thread.new { @pool.checkout } @@ -156,6 +196,48 @@ module ActiveRecord @pool.connections.each { |conn| conn.close if conn.in_use? } end + def test_idle_timeout_configuration + @pool.disconnect! + spec = ActiveRecord::Base.connection_pool.spec + spec.config.merge!(idle_timeout: "0.02") + @pool = ConnectionPool.new(spec) + idle_conn = @pool.checkout + @pool.checkin(idle_conn) + + idle_conn.instance_variable_set( + :@idle_since, + Concurrent.monotonic_time - 0.01 + ) + + @pool.flush + assert_equal 1, @pool.connections.length + + idle_conn.instance_variable_set( + :@idle_since, + Concurrent.monotonic_time - 0.02 + ) + + @pool.flush + assert_equal 0, @pool.connections.length + end + + def test_disable_flush + @pool.disconnect! + spec = ActiveRecord::Base.connection_pool.spec + spec.config.merge!(idle_timeout: -5) + @pool = ConnectionPool.new(spec) + idle_conn = @pool.checkout + @pool.checkin(idle_conn) + + idle_conn.instance_variable_set( + :@idle_since, + Concurrent.monotonic_time - 1 + ) + + @pool.flush + assert_equal 1, @pool.connections.length + end + def test_flush idle_conn = @pool.checkout recent_conn = @pool.checkout @@ -166,9 +248,10 @@ module ActiveRecord assert_equal 3, @pool.connections.length - def idle_conn.seconds_idle - 1000 - end + idle_conn.instance_variable_set( + :@idle_since, + Concurrent.monotonic_time - 1000 + ) @pool.flush(30) diff --git a/activerecord/test/cases/connection_specification/resolver_test.rb b/activerecord/test/cases/connection_specification/resolver_test.rb index 5b80f16a44..72be14f507 100644 --- a/activerecord/test/cases/connection_specification/resolver_test.rb +++ b/activerecord/test/cases/connection_specification/resolver_test.rb @@ -7,11 +7,15 @@ module ActiveRecord class ConnectionSpecification class ResolverTest < ActiveRecord::TestCase def resolve(spec, config = {}) - Resolver.new(config).resolve(spec) + configs = ActiveRecord::DatabaseConfigurations.new(config) + resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(configs) + resolver.resolve(spec, spec) end def spec(spec, config = {}) - Resolver.new(config).spec(spec) + configs = ActiveRecord::DatabaseConfigurations.new(config) + resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(configs) + resolver.spec(spec) end def test_url_invalid_adapter diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index 83cc2aa319..b1ebd20d6b 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -567,8 +567,6 @@ class DirtyTest < ActiveRecord::TestCase assert_not_nil pirate.previous_changes["updated_on"][1] assert_not pirate.previous_changes.key?("parrot_id") assert_not pirate.previous_changes.key?("created_on") - ensure - travel_back end class Testings < ActiveRecord::Base; end @@ -879,6 +877,26 @@ class DirtyTest < ActiveRecord::TestCase raise "changed? should be false" if changed? raise "has_changes_to_save? should be false" if has_changes_to_save? raise "saved_changes? should be true" unless saved_changes? + raise "id_in_database should not be nil" if id_in_database.nil? + end + end + + person = klass.create!(first_name: "Sean") + assert_not_predicate person, :changed? + end + + test "changed? in around callbacks after yield returns false" do + klass = Class.new(ActiveRecord::Base) do + self.table_name = "people" + + around_create :check_around + + def check_around + yield + raise "changed? should be false" if changed? + raise "has_changes_to_save? should be false" if has_changes_to_save? + raise "saved_changes? should be true" unless saved_changes? + raise "id_in_database should not be nil" if id_in_database.nil? end end diff --git a/activerecord/test/cases/explain_subscriber_test.rb b/activerecord/test/cases/explain_subscriber_test.rb index 82cc891970..79a0630193 100644 --- a/activerecord/test/cases/explain_subscriber_test.rb +++ b/activerecord/test/cases/explain_subscriber_test.rb @@ -40,7 +40,7 @@ if ActiveRecord::Base.connection.supports_explain? assert_equal binds, queries[0][1] end - def test_collects_nothing_if_the_statement_is_not_whitelisted + def test_collects_nothing_if_the_statement_is_not_explainable SUBSCRIBER.finish(nil, nil, name: "SQL", sql: "SHOW max_identifier_length") assert_empty queries end diff --git a/activerecord/test/cases/filter_attributes_test.rb b/activerecord/test/cases/filter_attributes_test.rb new file mode 100644 index 0000000000..f88cecfe2b --- /dev/null +++ b/activerecord/test/cases/filter_attributes_test.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +require "cases/helper" +require "models/admin" +require "models/admin/user" +require "models/admin/account" +require "pp" + +class FilterAttributesTest < ActiveRecord::TestCase + fixtures :"admin/users", :"admin/accounts" + + setup do + ActiveRecord::Base.filter_attributes = [:name] + end + + teardown do + ActiveRecord::Base.filter_attributes = [] + end + + test "filter_attributes" do + Admin::User.all.each do |user| + assert_includes user.inspect, "name: [FILTERED]" + assert_equal 1, user.inspect.scan("[FILTERED]").length + end + + Admin::Account.all.each do |account| + assert_includes account.inspect, "name: [FILTERED]" + assert_equal 1, account.inspect.scan("[FILTERED]").length + end + end + + test "filter_attributes could be overwritten by models" do + Admin::Account.all.each do |account| + assert_includes account.inspect, "name: [FILTERED]" + assert_equal 1, account.inspect.scan("[FILTERED]").length + end + + Admin::Account.filter_attributes = [] + + # Above changes should not impact other models + Admin::User.all.each do |user| + assert_includes user.inspect, "name: [FILTERED]" + assert_equal 1, user.inspect.scan("[FILTERED]").length + end + + Admin::Account.all.each do |account| + assert_not_includes account.inspect, "name: [FILTERED]" + assert_equal 0, account.inspect.scan("[FILTERED]").length + end + + Admin::Account.filter_attributes = [:name] + end + + test "filter_attributes should not filter nil value" do + account = Admin::Account.new + + assert_includes account.inspect, "name: nil" + assert_not_includes account.inspect, "name: [FILTERED]" + assert_equal 0, account.inspect.scan("[FILTERED]").length + end + + test "filter_attributes on pretty_print" do + user = admin_users(:david) + actual = "".dup + PP.pp(user, StringIO.new(actual)) + + assert_includes actual, "name: [FILTERED]" + assert_equal 1, actual.scan("[FILTERED]").length + end + + test "filter_attributes on pretty_print should not filter nil value" do + user = Admin::User.new + actual = "".dup + PP.pp(user, StringIO.new(actual)) + + assert_includes actual, "name: nil" + assert_not_includes actual, "name: [FILTERED]" + assert_equal 0, actual.scan("[FILTERED]").length + end +end diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index 66f11fe5bd..68be685e4b 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -183,5 +183,3 @@ module InTimeZone ActiveRecord::Base.time_zone_aware_attributes = old_tz end end - -require "mocha/minitest" # FIXME: stop using mocha diff --git a/activerecord/test/cases/legacy_configurations_test.rb b/activerecord/test/cases/legacy_configurations_test.rb new file mode 100644 index 0000000000..c36feb5116 --- /dev/null +++ b/activerecord/test/cases/legacy_configurations_test.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require "cases/helper" + +module ActiveRecord + class LegacyConfigurationsTest < ActiveRecord::TestCase + def test_can_turn_configurations_into_a_hash + assert ActiveRecord::Base.configurations.to_h.is_a?(Hash), "expected to be a hash but was not." + assert_equal ["arunit", "arunit2", "arunit_without_prepared_statements"].sort, ActiveRecord::Base.configurations.to_h.keys.sort + end + + def test_each_is_deprecated + assert_deprecated do + ActiveRecord::Base.configurations.each do |db_config| + assert_equal "primary", db_config.spec_name + end + end + end + + def test_first_is_deprecated + assert_deprecated do + db_config = ActiveRecord::Base.configurations.first + assert_equal "arunit", db_config.env_name + assert_equal "primary", db_config.spec_name + end + end + + def test_fetch_is_deprecated + assert_deprecated do + db_config = ActiveRecord::Base.configurations.fetch("arunit").first + assert_equal "arunit", db_config.env_name + assert_equal "primary", db_config.spec_name + end + end + + def test_values_are_deprecated + config_hashes = ActiveRecord::Base.configurations.configurations.map(&:config) + assert_deprecated do + assert_equal config_hashes, ActiveRecord::Base.configurations.values + end + end + end +end diff --git a/activerecord/test/cases/log_subscriber_test.rb b/activerecord/test/cases/log_subscriber_test.rb index f0126fdb0d..ae2597adc8 100644 --- a/activerecord/test/cases/log_subscriber_test.rb +++ b/activerecord/test/cases/log_subscriber_test.rb @@ -44,6 +44,7 @@ class LogSubscriberTest < ActiveRecord::TestCase def setup @old_logger = ActiveRecord::Base.logger Developer.primary_key + ActiveRecord::Base.connection.materialize_transactions super ActiveRecord::LogSubscriber.attach_to(:active_record) end diff --git a/activerecord/test/cases/migration/command_recorder_test.rb b/activerecord/test/cases/migration/command_recorder_test.rb index 3a11bb081b..1a19b8dafd 100644 --- a/activerecord/test/cases/migration/command_recorder_test.rb +++ b/activerecord/test/cases/migration/command_recorder_test.rb @@ -329,11 +329,24 @@ module ActiveRecord assert_equal [:add_foreign_key, [:dogs, :people, primary_key: "person_id"]], enable end + def test_invert_remove_foreign_key_with_primary_key_and_to_table_in_options + enable = @recorder.inverse_of :remove_foreign_key, [:dogs, to_table: :people, primary_key: "uuid"] + assert_equal [:add_foreign_key, [:dogs, :people, primary_key: "uuid"]], enable + end + def test_invert_remove_foreign_key_with_on_delete_on_update enable = @recorder.inverse_of :remove_foreign_key, [:dogs, :people, on_delete: :nullify, on_update: :cascade] assert_equal [:add_foreign_key, [:dogs, :people, on_delete: :nullify, on_update: :cascade]], enable end + def test_invert_remove_foreign_key_with_to_table_in_options + enable = @recorder.inverse_of :remove_foreign_key, [:dogs, to_table: :people] + assert_equal [:add_foreign_key, [:dogs, :people]], enable + + enable = @recorder.inverse_of :remove_foreign_key, [:dogs, to_table: :people, column: :owner_id] + assert_equal [:add_foreign_key, [:dogs, :people, column: :owner_id]], enable + end + def test_invert_remove_foreign_key_is_irreversible_without_to_table assert_raises ActiveRecord::IrreversibleMigration do @recorder.inverse_of :remove_foreign_key, [:dogs, column: "owner_id"] diff --git a/activerecord/test/cases/migration/foreign_key_test.rb b/activerecord/test/cases/migration/foreign_key_test.rb index c471dd1106..bb233fbf74 100644 --- a/activerecord/test/cases/migration/foreign_key_test.rb +++ b/activerecord/test/cases/migration/foreign_key_test.rb @@ -53,16 +53,49 @@ if ActiveRecord::Base.connection.supports_foreign_keys_in_create? end def test_change_column_of_parent_table - foreign_keys = ActiveRecord::Base.connection.foreign_keys("astronauts") rocket = Rocket.create!(name: "myrocket") rocket.astronauts << Astronaut.create! @connection.change_column_null :rockets, :name, false + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + assert_equal "myrocket", Rocket.first.name + assert_equal "astronauts", fk.from_table + assert_equal "rockets", fk.to_table + end + + def test_rename_column_of_child_table + rocket = Rocket.create!(name: "myrocket") + rocket.astronauts << Astronaut.create! + + @connection.rename_column :astronauts, :name, :astronaut_name + + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + assert_equal "myrocket", Rocket.first.name + assert_equal "astronauts", fk.from_table + assert_equal "rockets", fk.to_table + end + + def test_rename_reference_column_of_child_table + rocket = Rocket.create!(name: "myrocket") + rocket.astronauts << Astronaut.create! + + @connection.rename_column :astronauts, :rocket_id, :new_rocket_id + + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + fk = foreign_keys.first assert_equal "myrocket", Rocket.first.name assert_equal "astronauts", fk.from_table assert_equal "rockets", fk.to_table + assert_equal "new_rocket_id", fk.options[:column] end end end diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index d1292dc53d..868bb40ab2 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -793,12 +793,20 @@ if ActiveRecord::Base.connection.supports_bulk_alter? end def test_adding_multiple_columns - assert_queries(1) do + classname = ActiveRecord::Base.connection.class.name[/[^:]*$/] + expected_query_count = { + "Mysql2Adapter" => 1, + "PostgreSQLAdapter" => 2, # one for bulk change, one for comment + }.fetch(classname) { + raise "need an expected query count for #{classname}" + } + + assert_queries(expected_query_count) do with_bulk_change_table do |t| t.column :name, :string t.string :qualification, :experience t.integer :age, default: 0 - t.date :birthdate + t.date :birthdate, comment: "This is a comment" t.timestamps null: true end end @@ -806,6 +814,7 @@ if ActiveRecord::Base.connection.supports_bulk_alter? assert_equal 8, columns.size [:name, :qualification, :experience].each { |s| assert_equal :string, column(s).type } assert_equal "0", column(:age).default + assert_equal "This is a comment", column(:birthdate).comment end def test_removing_columns diff --git a/activerecord/test/cases/reaper_test.rb b/activerecord/test/cases/reaper_test.rb index b034fe3e3b..b630f782bc 100644 --- a/activerecord/test/cases/reaper_test.rb +++ b/activerecord/test/cases/reaper_test.rb @@ -61,9 +61,9 @@ module ActiveRecord def test_reaping_frequency_configuration spec = ActiveRecord::Base.connection_pool.spec.dup - spec.config[:reaping_frequency] = 100 + spec.config[:reaping_frequency] = "10.01" pool = ConnectionPool.new spec - assert_equal 100, pool.reaper.frequency + assert_equal 10.01, pool.reaper.frequency end def test_connection_pool_starts_reaper diff --git a/activerecord/test/cases/relation/delegation_test.rb b/activerecord/test/cases/relation/delegation_test.rb index 3f3d41980c..a8030c2d64 100644 --- a/activerecord/test/cases/relation/delegation_test.rb +++ b/activerecord/test/cases/relation/delegation_test.rb @@ -5,7 +5,7 @@ require "models/post" require "models/comment" module ActiveRecord - module DelegationWhitelistTests + module ArrayDelegationTests ARRAY_DELEGATES = [ :+, :-, :|, :&, :[], :shuffle, :all?, :collect, :compact, :detect, :each, :each_cons, :each_with_index, @@ -38,7 +38,7 @@ module ActiveRecord end class DelegationAssociationTest < ActiveRecord::TestCase - include DelegationWhitelistTests + include ArrayDelegationTests include DeprecatedArelDelegationTests def target @@ -47,7 +47,7 @@ module ActiveRecord end class DelegationRelationTest < ActiveRecord::TestCase - include DelegationWhitelistTests + include ArrayDelegationTests include DeprecatedArelDelegationTests def target diff --git a/activerecord/test/cases/relation/merging_test.rb b/activerecord/test/cases/relation/merging_test.rb index f53ef1fe35..6e7998d15a 100644 --- a/activerecord/test/cases/relation/merging_test.rb +++ b/activerecord/test/cases/relation/merging_test.rb @@ -121,6 +121,16 @@ class RelationMergingTest < ActiveRecord::TestCase relation = relation.merge(Post.from("posts")) assert_not_empty relation.from_clause end + + def test_merging_with_order_with_binds + relation = Post.all.merge(Post.order([Arel.sql("title LIKE ?"), "%suffix"])) + assert_equal ["title LIKE '%suffix'"], relation.order_values + end + + def test_merging_with_order_without_binds + relation = Post.all.merge(Post.order(Arel.sql("title LIKE '%?'"))) + assert_equal ["title LIKE '%?'"], relation.order_values + end end class MergingDifferentRelationsTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 5412ab5def..d03b412efb 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -1587,6 +1587,24 @@ class RelationTest < ActiveRecord::TestCase assert_equal "David", topic2.reload.author_name end + def test_update_with_ids_on_relation + topic1 = TopicWithCallbacks.create!(title: "arel", author_name: nil) + topic2 = TopicWithCallbacks.create!(title: "activerecord", author_name: nil) + topics = TopicWithCallbacks.none + topics.update( + [topic1.id, topic2.id], + [{ title: "adequaterecord" }, { title: "adequaterecord" }] + ) + + assert_equal TopicWithCallbacks.count, TopicWithCallbacks.topic_count + + assert_equal "adequaterecord", topic1.reload.title + assert_equal "adequaterecord", topic2.reload.title + # Testing that the before_update callbacks have run + assert_equal "David", topic1.reload.author_name + assert_equal "David", topic2.reload.author_name + end + def test_update_on_relation_passing_active_record_object_is_not_permitted topic = Topic.create!(title: "Foo", author_name: nil) assert_raises(ArgumentError) do diff --git a/activerecord/test/cases/serialized_attribute_test.rb b/activerecord/test/cases/serialized_attribute_test.rb index 4cd4515c3b..1192b30b14 100644 --- a/activerecord/test/cases/serialized_attribute_test.rb +++ b/activerecord/test/cases/serialized_attribute_test.rb @@ -322,7 +322,7 @@ class SerializedAttributeTest < ActiveRecord::TestCase topic = Topic.create!(content: {}) topic2 = Topic.create!(content: nil) - assert_equal [topic, topic2], Topic.where(content: nil) + assert_equal [topic, topic2], Topic.where(content: nil).sort_by(&:id) end def test_nil_is_always_persisted_as_null diff --git a/activerecord/test/cases/tasks/database_tasks_test.rb b/activerecord/test/cases/tasks/database_tasks_test.rb index a6efe3fa5e..d674bd562f 100644 --- a/activerecord/test/cases/tasks/database_tasks_test.rb +++ b/activerecord/test/cases/tasks/database_tasks_test.rb @@ -46,35 +46,46 @@ module ActiveRecord class DatabaseTasksUtilsTask < ActiveRecord::TestCase def test_raises_an_error_when_called_with_protected_environment - ActiveRecord::MigrationContext.any_instance.stubs(:current_version).returns(1) - protected_environments = ActiveRecord::Base.protected_environments current_env = ActiveRecord::Base.connection.migration_context.current_environment - assert_not_includes protected_environments, current_env - # Assert no error - ActiveRecord::Tasks::DatabaseTasks.check_protected_environments! - - ActiveRecord::Base.protected_environments = [current_env] - assert_raise(ActiveRecord::ProtectedEnvironmentError) do + assert_called_on_instance_of( + ActiveRecord::MigrationContext, + :current_version, + times: 6, + returns: 1 + ) do + assert_not_includes protected_environments, current_env + # Assert no error ActiveRecord::Tasks::DatabaseTasks.check_protected_environments! + + ActiveRecord::Base.protected_environments = [current_env] + + assert_raise(ActiveRecord::ProtectedEnvironmentError) do + ActiveRecord::Tasks::DatabaseTasks.check_protected_environments! + end end ensure ActiveRecord::Base.protected_environments = protected_environments end def test_raises_an_error_when_called_with_protected_environment_which_name_is_a_symbol - ActiveRecord::MigrationContext.any_instance.stubs(:current_version).returns(1) - protected_environments = ActiveRecord::Base.protected_environments current_env = ActiveRecord::Base.connection.migration_context.current_environment - assert_not_includes protected_environments, current_env - # Assert no error - ActiveRecord::Tasks::DatabaseTasks.check_protected_environments! - - ActiveRecord::Base.protected_environments = [current_env.to_sym] - assert_raise(ActiveRecord::ProtectedEnvironmentError) do + assert_called_on_instance_of( + ActiveRecord::MigrationContext, + :current_version, + times: 6, + returns: 1 + ) do + assert_not_includes protected_environments, current_env + # Assert no error ActiveRecord::Tasks::DatabaseTasks.check_protected_environments! + + ActiveRecord::Base.protected_environments = [current_env.to_sym] + assert_raise(ActiveRecord::ProtectedEnvironmentError) do + ActiveRecord::Tasks::DatabaseTasks.check_protected_environments! + end end ensure ActiveRecord::Base.protected_environments = protected_environments @@ -82,10 +93,14 @@ module ActiveRecord def test_raises_an_error_if_no_migrations_have_been_made ActiveRecord::InternalMetadata.stub(:table_exists?, false) do - ActiveRecord::MigrationContext.any_instance.stubs(:current_version).returns(1) - - assert_raise(ActiveRecord::NoEnvironmentInSchemaError) do - ActiveRecord::Tasks::DatabaseTasks.check_protected_environments! + assert_called_on_instance_of( + ActiveRecord::MigrationContext, + :current_version, + returns: 1 + ) do + assert_raise(ActiveRecord::NoEnvironmentInSchemaError) do + ActiveRecord::Tasks::DatabaseTasks.check_protected_environments! + end end end end @@ -213,18 +228,17 @@ module ActiveRecord end private - def with_stubbed_configurations_establish_connection - ActiveRecord::Base.stub(:configurations, @configurations) do - # To refrain from connecting to a newly created empty DB in - # sqlite3_mem tests - ActiveRecord::Base.connection_handler.stub( - :establish_connection, - nil - ) do - yield - end + old_configurations = ActiveRecord::Base.configurations + ActiveRecord::Base.configurations = @configurations + + # To refrain from connecting to a newly created empty DB in + # sqlite3_mem tests + ActiveRecord::Base.connection_handler.stub(:establish_connection, nil) do + yield end + ensure + ActiveRecord::Base.configurations = old_configurations end end @@ -233,7 +247,7 @@ module ActiveRecord @configurations = { "development" => { "database" => "dev-db" }, "test" => { "database" => "test-db" }, - "production" => { "url" => "prod-db-url" } + "production" => { "url" => "abstract://prod-db-host/prod-db" } } end @@ -256,7 +270,7 @@ module ActiveRecord assert_called_with( ActiveRecord::Tasks::DatabaseTasks, :create, - ["url" => "prod-db-url"], + ["adapter" => "abstract", "database" => "prod-db", "host" => "prod-db-host"], ) do ActiveRecord::Tasks::DatabaseTasks.create_current( ActiveSupport::StringInquirer.new("production") @@ -315,13 +329,15 @@ module ActiveRecord end private - def with_stubbed_configurations_establish_connection - ActiveRecord::Base.stub(:configurations, @configurations) do - ActiveRecord::Base.stub(:establish_connection, nil) do - yield - end + old_configurations = ActiveRecord::Base.configurations + ActiveRecord::Base.configurations = @configurations + + ActiveRecord::Base.connection_handler.stub(:establish_connection, nil) do + yield end + ensure + ActiveRecord::Base.configurations = old_configurations end end @@ -330,7 +346,7 @@ module ActiveRecord @configurations = { "development" => { "primary" => { "database" => "dev-db" }, "secondary" => { "database" => "secondary-dev-db" } }, "test" => { "primary" => { "database" => "test-db" }, "secondary" => { "database" => "secondary-test-db" } }, - "production" => { "primary" => { "url" => "prod-db-url" }, "secondary" => { "url" => "secondary-prod-db-url" } } + "production" => { "primary" => { "url" => "abstract://prod-db-host/prod-db" }, "secondary" => { "url" => "abstract://secondary-prod-db-host/secondary-prod-db" } } } end @@ -357,8 +373,8 @@ module ActiveRecord ActiveRecord::Tasks::DatabaseTasks, :create, [ - ["url" => "prod-db-url"], - ["url" => "secondary-prod-db-url"] + ["adapter" => "abstract", "database" => "prod-db", "host" => "prod-db-host"], + ["adapter" => "abstract", "database" => "secondary-prod-db", "host" => "secondary-prod-db-host"] ] ) do ActiveRecord::Tasks::DatabaseTasks.create_current( @@ -426,13 +442,15 @@ module ActiveRecord end private - def with_stubbed_configurations_establish_connection - ActiveRecord::Base.stub(:configurations, @configurations) do - ActiveRecord::Base.stub(:establish_connection, nil) do - yield - end + old_configurations = ActiveRecord::Base.configurations + ActiveRecord::Base.configurations = @configurations + + ActiveRecord::Base.connection_handler.stub(:establish_connection, nil) do + yield end + ensure + ActiveRecord::Base.configurations = old_configurations end end @@ -465,7 +483,7 @@ module ActiveRecord def test_ignores_configurations_without_databases @configurations[:development]["database"] = nil - ActiveRecord::Base.stub(:configurations, @configurations) do + with_stubbed_configurations do assert_not_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do ActiveRecord::Tasks::DatabaseTasks.drop_all end @@ -475,7 +493,7 @@ module ActiveRecord def test_ignores_remote_databases @configurations[:development]["host"] = "my.server.tld" - ActiveRecord::Base.stub(:configurations, @configurations) do + with_stubbed_configurations do assert_not_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do ActiveRecord::Tasks::DatabaseTasks.drop_all end @@ -485,7 +503,7 @@ module ActiveRecord def test_warning_for_remote_databases @configurations[:development]["host"] = "my.server.tld" - ActiveRecord::Base.stub(:configurations, @configurations) do + with_stubbed_configurations do ActiveRecord::Tasks::DatabaseTasks.drop_all assert_match "This task only modifies local databases. my-db is on a remote host.", @@ -496,7 +514,7 @@ module ActiveRecord def test_drops_configurations_with_local_ip @configurations[:development]["host"] = "127.0.0.1" - ActiveRecord::Base.stub(:configurations, @configurations) do + with_stubbed_configurations do assert_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do ActiveRecord::Tasks::DatabaseTasks.drop_all end @@ -506,7 +524,7 @@ module ActiveRecord def test_drops_configurations_with_local_host @configurations[:development]["host"] = "localhost" - ActiveRecord::Base.stub(:configurations, @configurations) do + with_stubbed_configurations do assert_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do ActiveRecord::Tasks::DatabaseTasks.drop_all end @@ -516,12 +534,22 @@ module ActiveRecord def test_drops_configurations_with_blank_hosts @configurations[:development]["host"] = nil - ActiveRecord::Base.stub(:configurations, @configurations) do + with_stubbed_configurations do assert_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do ActiveRecord::Tasks::DatabaseTasks.drop_all end end end + + private + def with_stubbed_configurations + old_configurations = ActiveRecord::Base.configurations + ActiveRecord::Base.configurations = @configurations + + yield + ensure + ActiveRecord::Base.configurations = old_configurations + end end class DatabaseTasksDropCurrentTest < ActiveRecord::TestCase @@ -529,12 +557,12 @@ module ActiveRecord @configurations = { "development" => { "database" => "dev-db" }, "test" => { "database" => "test-db" }, - "production" => { "url" => "prod-db-url" } + "production" => { "url" => "abstract://prod-db-host/prod-db" } } end def test_drops_current_environment_database - ActiveRecord::Base.stub(:configurations, @configurations) do + with_stubbed_configurations do assert_called_with(ActiveRecord::Tasks::DatabaseTasks, :drop, ["database" => "test-db"]) do ActiveRecord::Tasks::DatabaseTasks.drop_current( @@ -545,9 +573,9 @@ module ActiveRecord end def test_drops_current_environment_database_with_url - ActiveRecord::Base.stub(:configurations, @configurations) do + with_stubbed_configurations do assert_called_with(ActiveRecord::Tasks::DatabaseTasks, :drop, - ["url" => "prod-db-url"]) do + ["adapter" => "abstract", "database" => "prod-db", "host" => "prod-db-host"]) do ActiveRecord::Tasks::DatabaseTasks.drop_current( ActiveSupport::StringInquirer.new("production") ) @@ -556,7 +584,7 @@ module ActiveRecord end def test_drops_test_and_development_databases_when_env_was_not_specified - ActiveRecord::Base.stub(:configurations, @configurations) do + with_stubbed_configurations do assert_called_with( ActiveRecord::Tasks::DatabaseTasks, :drop, @@ -576,7 +604,7 @@ module ActiveRecord old_env = ENV["RAILS_ENV"] ENV["RAILS_ENV"] = "development" - ActiveRecord::Base.stub(:configurations, @configurations) do + with_stubbed_configurations do assert_called_with( ActiveRecord::Tasks::DatabaseTasks, :drop, @@ -593,6 +621,16 @@ module ActiveRecord ensure ENV["RAILS_ENV"] = old_env end + + private + def with_stubbed_configurations + old_configurations = ActiveRecord::Base.configurations + ActiveRecord::Base.configurations = @configurations + + yield + ensure + ActiveRecord::Base.configurations = old_configurations + end end class DatabaseTasksDropCurrentThreeTierTest < ActiveRecord::TestCase @@ -600,12 +638,12 @@ module ActiveRecord @configurations = { "development" => { "primary" => { "database" => "dev-db" }, "secondary" => { "database" => "secondary-dev-db" } }, "test" => { "primary" => { "database" => "test-db" }, "secondary" => { "database" => "secondary-test-db" } }, - "production" => { "primary" => { "url" => "prod-db-url" }, "secondary" => { "url" => "secondary-prod-db-url" } } + "production" => { "primary" => { "url" => "abstract://prod-db-host/prod-db" }, "secondary" => { "url" => "abstract://secondary-prod-db-host/secondary-prod-db" } } } end def test_drops_current_environment_database - ActiveRecord::Base.stub(:configurations, @configurations) do + with_stubbed_configurations do assert_called_with( ActiveRecord::Tasks::DatabaseTasks, :drop, @@ -622,13 +660,13 @@ module ActiveRecord end def test_drops_current_environment_database_with_url - ActiveRecord::Base.stub(:configurations, @configurations) do + with_stubbed_configurations do assert_called_with( ActiveRecord::Tasks::DatabaseTasks, :drop, [ - ["url" => "prod-db-url"], - ["url" => "secondary-prod-db-url"] + ["adapter" => "abstract", "database" => "prod-db", "host" => "prod-db-host"], + ["adapter" => "abstract", "database" => "secondary-prod-db", "host" => "secondary-prod-db-host"] ] ) do ActiveRecord::Tasks::DatabaseTasks.drop_current( @@ -639,7 +677,7 @@ module ActiveRecord end def test_drops_test_and_development_databases_when_env_was_not_specified - ActiveRecord::Base.stub(:configurations, @configurations) do + with_stubbed_configurations do assert_called_with( ActiveRecord::Tasks::DatabaseTasks, :drop, @@ -661,7 +699,7 @@ module ActiveRecord old_env = ENV["RAILS_ENV"] ENV["RAILS_ENV"] = "development" - ActiveRecord::Base.stub(:configurations, @configurations) do + with_stubbed_configurations do assert_called_with( ActiveRecord::Tasks::DatabaseTasks, :drop, @@ -680,6 +718,16 @@ module ActiveRecord ensure ENV["RAILS_ENV"] = old_env end + + private + def with_stubbed_configurations + old_configurations = ActiveRecord::Base.configurations + ActiveRecord::Base.configurations = @configurations + + yield + ensure + ActiveRecord::Base.configurations = old_configurations + end end if current_adapter?(:SQLite3Adapter) && !in_memory_db? @@ -833,37 +881,44 @@ module ActiveRecord class DatabaseTasksPurgeCurrentTest < ActiveRecord::TestCase def test_purges_current_environment_database + old_configurations = ActiveRecord::Base.configurations configurations = { "development" => { "database" => "dev-db" }, "test" => { "database" => "test-db" }, "production" => { "database" => "prod-db" } } - ActiveRecord::Base.stub(:configurations, configurations) do - assert_called_with( - ActiveRecord::Tasks::DatabaseTasks, - :purge, - ["database" => "prod-db"] - ) do - assert_called_with(ActiveRecord::Base, :establish_connection, [:production]) do - ActiveRecord::Tasks::DatabaseTasks.purge_current("production") - end + + ActiveRecord::Base.configurations = configurations + + assert_called_with( + ActiveRecord::Tasks::DatabaseTasks, + :purge, + ["database" => "prod-db"] + ) do + assert_called_with(ActiveRecord::Base, :establish_connection, [:production]) do + ActiveRecord::Tasks::DatabaseTasks.purge_current("production") end end + ensure + ActiveRecord::Base.configurations = old_configurations end end class DatabaseTasksPurgeAllTest < ActiveRecord::TestCase def test_purge_all_local_configurations + old_configurations = ActiveRecord::Base.configurations configurations = { development: { "database" => "my-db" } } - ActiveRecord::Base.stub(:configurations, configurations) do - assert_called_with( - ActiveRecord::Tasks::DatabaseTasks, - :purge, - ["database" => "my-db"] - ) do - ActiveRecord::Tasks::DatabaseTasks.purge_all - end + ActiveRecord::Base.configurations = configurations + + assert_called_with( + ActiveRecord::Tasks::DatabaseTasks, + :purge, + ["database" => "my-db"] + ) do + ActiveRecord::Tasks::DatabaseTasks.purge_all end + ensure + ActiveRecord::Base.configurations = old_configurations end end diff --git a/activerecord/test/cases/tasks/mysql_rake_test.rb b/activerecord/test/cases/tasks/mysql_rake_test.rb index eeb4222d97..4d6dff68f9 100644 --- a/activerecord/test/cases/tasks/mysql_rake_test.rb +++ b/activerecord/test/cases/tasks/mysql_rake_test.rb @@ -152,10 +152,14 @@ if current_adapter?(:Mysql2Adapter) end def test_establishes_connection_to_mysql_database - with_stubbed_connection_establish_connection do - ActiveRecord::Base.expects(:establish_connection).with @configuration - - ActiveRecord::Tasks::DatabaseTasks.drop @configuration + ActiveRecord::Base.stub(:connection, @connection) do + assert_called_with( + ActiveRecord::Base, + :establish_connection, + [@configuration] + ) do + ActiveRecord::Tasks::DatabaseTasks.drop @configuration + end end end @@ -196,10 +200,14 @@ if current_adapter?(:Mysql2Adapter) end def test_establishes_connection_to_the_appropriate_database - with_stubbed_connection_establish_connection do - ActiveRecord::Base.expects(:establish_connection).with(@configuration) - - ActiveRecord::Tasks::DatabaseTasks.purge @configuration + ActiveRecord::Base.stub(:connection, @connection) do + assert_called_with( + ActiveRecord::Base, + :establish_connection, + [@configuration] + ) do + ActiveRecord::Tasks::DatabaseTasks.purge @configuration + end end end diff --git a/activerecord/test/cases/tasks/postgresql_rake_test.rb b/activerecord/test/cases/tasks/postgresql_rake_test.rb index 00005e7a0d..8c6e8d79f3 100644 --- a/activerecord/test/cases/tasks/postgresql_rake_test.rb +++ b/activerecord/test/cases/tasks/postgresql_rake_test.rb @@ -112,7 +112,7 @@ if current_adapter?(:PostgreSQLAdapter) ActiveRecord::Base.stub(:connection, @connection) do ActiveRecord::Base.stub(:establish_connection, -> * { raise Exception }) do assert_raises(Exception) { ActiveRecord::Tasks::DatabaseTasks.create @configuration } - assert_match "Couldn't create database for #{@configuration.inspect}", $stderr.string + assert_match "Couldn't create '#{@configuration['database']}' database. Please check your configuration.", $stderr.string end end end @@ -166,12 +166,17 @@ if current_adapter?(:PostgreSQLAdapter) def test_establishes_connection_to_postgresql_database ActiveRecord::Base.stub(:connection, @connection) do - ActiveRecord::Base.expects(:establish_connection).with( - "adapter" => "postgresql", - "database" => "postgres", - "schema_search_path" => "public" - ) - ActiveRecord::Tasks::DatabaseTasks.drop @configuration + assert_called_with( + ActiveRecord::Base, + :establish_connection, + [ + "adapter" => "postgresql", + "database" => "postgres", + "schema_search_path" => "public" + ] + ) do + ActiveRecord::Tasks::DatabaseTasks.drop @configuration + end end end diff --git a/activerecord/test/cases/tasks/sqlite_rake_test.rb b/activerecord/test/cases/tasks/sqlite_rake_test.rb index 7eb062b456..c1092b97c1 100644 --- a/activerecord/test/cases/tasks/sqlite_rake_test.rb +++ b/activerecord/test/cases/tasks/sqlite_rake_test.rb @@ -47,9 +47,9 @@ if current_adapter?(:SQLite3Adapter) def test_db_create_with_file_does_nothing File.stub(:exist?, true) do - ActiveRecord::Base.expects(:establish_connection).never - - ActiveRecord::Tasks::DatabaseTasks.create @configuration, "/rails/root" + assert_not_called(ActiveRecord::Base, :establish_connection) do + ActiveRecord::Tasks::DatabaseTasks.create @configuration, "/rails/root" + end end end @@ -62,7 +62,7 @@ if current_adapter?(:SQLite3Adapter) def test_db_create_with_error_prints_message ActiveRecord::Base.stub(:establish_connection, proc { raise Exception }) do assert_raises(Exception) { ActiveRecord::Tasks::DatabaseTasks.create @configuration, "/rails/root" } - assert_match "Couldn't create database for #{@configuration.inspect}", $stderr.string + assert_match "Couldn't create '#{@configuration['database']}' database. Please check your configuration.", $stderr.string end end end diff --git a/activerecord/test/cases/test_case.rb b/activerecord/test/cases/test_case.rb index 409b07e56c..40947767f3 100644 --- a/activerecord/test/cases/test_case.rb +++ b/activerecord/test/cases/test_case.rb @@ -31,6 +31,7 @@ module ActiveRecord end def capture_sql + ActiveRecord::Base.connection.materialize_transactions SQLCounter.clear_log yield SQLCounter.log_all.dup @@ -48,6 +49,7 @@ module ActiveRecord def assert_queries(num = 1, options = {}) ignore_none = options.fetch(:ignore_none) { num == :any } + ActiveRecord::Base.connection.materialize_transactions SQLCounter.clear_log x = yield the_log = ignore_none ? SQLCounter.log_all : SQLCounter.log diff --git a/activerecord/test/cases/transaction_isolation_test.rb b/activerecord/test/cases/transaction_isolation_test.rb index eaafd13360..9663955f1f 100644 --- a/activerecord/test/cases/transaction_isolation_test.rb +++ b/activerecord/test/cases/transaction_isolation_test.rb @@ -11,7 +11,7 @@ unless ActiveRecord::Base.connection.supports_transaction_isolation? test "setting the isolation level raises an error" do assert_raises(ActiveRecord::TransactionIsolationError) do - Tag.transaction(isolation: :serializable) {} + Tag.transaction(isolation: :serializable) { Tag.connection.materialize_transactions } end end end diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb index 46463ac414..b13cf88c00 100644 --- a/activerecord/test/cases/transactions_test.rb +++ b/activerecord/test/cases/transactions_test.rb @@ -575,7 +575,7 @@ class TransactionTest < ActiveRecord::TestCase assert_called(Topic.connection, :rollback_db_transaction) do e = assert_raise RuntimeError do Topic.transaction do - # do nothing + Topic.connection.materialize_transactions end end assert_equal "OH NOES", e.message @@ -943,6 +943,76 @@ class TransactionTest < ActiveRecord::TestCase connection.drop_table "transaction_without_primary_keys", if_exists: true end + def test_empty_transaction_is_not_materialized + assert_no_queries do + Topic.transaction {} + end + end + + def test_unprepared_statement_materializes_transaction + assert_sql(/BEGIN/i, /COMMIT/i) do + Topic.transaction { Topic.where("1=1").first } + end + end + + if ActiveRecord::Base.connection.prepared_statements + def test_prepared_statement_materializes_transaction + Topic.first + + assert_sql(/BEGIN/i, /COMMIT/i) do + Topic.transaction { Topic.first } + end + end + end + + def test_savepoint_does_not_materialize_transaction + assert_no_queries do + Topic.transaction do + Topic.transaction(requires_new: true) {} + end + end + end + + def test_raising_does_not_materialize_transaction + assert_raise(RuntimeError) do + assert_no_queries do + Topic.transaction { raise } + end + end + end + + def test_accessing_raw_connection_materializes_transaction + assert_sql(/BEGIN/i, /COMMIT/i) do + Topic.transaction { Topic.connection.raw_connection } + end + end + + def test_accessing_raw_connection_disables_lazy_transactions + Topic.connection.raw_connection + + assert_sql(/BEGIN/i, /COMMIT/i) do + Topic.transaction {} + end + end + + def test_checking_in_connection_reenables_lazy_transactions + connection = Topic.connection_pool.checkout + connection.raw_connection + Topic.connection_pool.checkin connection + + assert_no_queries do + connection.transaction {} + end + end + + def test_transactions_can_be_manually_materialized + assert_sql(/BEGIN/i, /COMMIT/i) do + Topic.transaction do + Topic.connection.materialize_transactions + end + end + end + private %w(validation save destroy).each do |filter| diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb index a33877f43a..66763c727f 100644 --- a/activerecord/test/cases/validations_test.rb +++ b/activerecord/test/cases/validations_test.rb @@ -3,11 +3,11 @@ require "cases/helper" require "models/topic" require "models/reply" -require "models/person" require "models/developer" require "models/computer" require "models/parrot" require "models/company" +require "models/price_estimate" class ValidationsTest < ActiveRecord::TestCase fixtures :topics, :developers @@ -183,6 +183,22 @@ class ValidationsTest < ActiveRecord::TestCase assert_not_predicate klass.new(wibble: BigDecimal("97.179")), :valid? end + def test_numericality_validator_wont_be_affected_by_custom_getter + price_estimate = PriceEstimate.new(price: 50) + + assert_equal "$50.00", price_estimate.price + assert_equal 50, price_estimate.price_before_type_cast + assert_equal 50, price_estimate.read_attribute(:price) + + assert_predicate price_estimate, :price_came_from_user? + assert_predicate price_estimate, :valid? + + price_estimate.save! + + assert_not_predicate price_estimate, :price_came_from_user? + assert_predicate price_estimate, :valid? + end + def test_acceptance_validator_doesnt_require_db_connection klass = Class.new(ActiveRecord::Base) do self.table_name = "posts" diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb index d4d5275b78..485b35d58b 100644 --- a/activerecord/test/models/company.rb +++ b/activerecord/test/models/company.rb @@ -204,4 +204,12 @@ end class VerySpecialClient < SpecialClient end +class NewlyContractedCompany < Company + has_many :new_contracts, foreign_key: "company_id" + + before_save do + self.new_contracts << NewContract.new + end +end + require "models/account" diff --git a/activerecord/test/models/contract.rb b/activerecord/test/models/contract.rb index f273badd85..3f663375c4 100644 --- a/activerecord/test/models/contract.rb +++ b/activerecord/test/models/contract.rb @@ -20,3 +20,7 @@ class Contract < ActiveRecord::Base @bye_count += 1 end end + +class NewContract < Contract + validates :company_id, presence: true +end diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index 640cdb33b4..528585fb75 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -324,7 +324,7 @@ class FakeKlass table[name] end - def enforce_raw_sql_whitelist(*args) + def disallow_raw_sql!(*args) # noop end diff --git a/activerecord/test/models/price_estimate.rb b/activerecord/test/models/price_estimate.rb index f1f88d8d8d..669d0991f7 100644 --- a/activerecord/test/models/price_estimate.rb +++ b/activerecord/test/models/price_estimate.rb @@ -1,6 +1,14 @@ # frozen_string_literal: true class PriceEstimate < ActiveRecord::Base + include ActiveSupport::NumberHelper + belongs_to :estimate_of, polymorphic: true belongs_to :thing, polymorphic: true + + validates_numericality_of :price + + def price + number_to_currency super + end end diff --git a/activerecord/test/schema/mysql2_specific_schema.rb b/activerecord/test/schema/mysql2_specific_schema.rb index 8371ba9528..0f2f6ddd68 100644 --- a/activerecord/test/schema/mysql2_specific_schema.rb +++ b/activerecord/test/schema/mysql2_specific_schema.rb @@ -14,6 +14,13 @@ ActiveRecord::Schema.define do end end + create_table :defaults, force: true do |t| + t.date :fixed_date, default: "2004-01-01" + t.datetime :fixed_time, default: "2004-01-01 00:00:00" + t.column :char1, "char(1)", default: "Y" + t.string :char2, limit: 50, default: "a varchar field" + end + create_table :binary_fields, force: true do |t| t.binary :var_binary, limit: 255 t.binary :var_binary_large, limit: 4095 diff --git a/activerecord/test/schema/sqlite_specific_schema.rb b/activerecord/test/schema/sqlite_specific_schema.rb new file mode 100644 index 0000000000..18192292e4 --- /dev/null +++ b/activerecord/test/schema/sqlite_specific_schema.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +ActiveRecord::Schema.define do + create_table :defaults, force: true do |t| + t.date :fixed_date, default: "2004-01-01" + t.datetime :fixed_time, default: "2004-01-01 00:00:00" + t.column :char1, "char(1)", default: "Y" + t.string :char2, limit: 50, default: "a varchar field" + t.text :char3, limit: 50, default: "a text field" + end +end |