diff options
Diffstat (limited to 'activerecord')
36 files changed, 311 insertions, 224 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 0bd3c2e78c..5d04551125 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,32 @@ +* Fix the generated `#to_param` method to use `omission: ''` so that + the resulting output is actually up to 20 characters, not + effectively 17 to leave room for the default "...". + Also call `#parameterize` before `#truncate` and make the + `separator: /-/` to maximize the information included in the + output. + + Fixes #23635. + + *Rob Biedenharn* + +* Ensure concurrent invocations of the connection reaper cannot allocate the + same connection to two threads. + + Fixes #25585. + + *Matthew Draper* + +* Inspecting an object with an associated array of over 10 elements no longer + truncates the array, preventing `inspect` from looping infinitely in some + cases. + + *Kevin McPhillips* + +* Removed the unused methods `ActiveRecord::Base.connection_id` and + `ActiveRecord::Base.connection_id=`. + + *Sean Griffin* + * Ensure hashes can be assigned to attributes created using `composed_of`. Fixes #25210. @@ -13,15 +42,15 @@ *Johannes Opper* -* Introduce ActiveRecord::TransactionSerializationError for catching +* Introduce `ActiveRecord::TransactionSerializationError` for catching transaction serialization failures or deadlocks. *Erol Fornoles* -* PostgreSQL: Fix db:structure:load silent failure on SQL error +* PostgreSQL: Fix db:structure:load silent failure on SQL error. - The command line flag "-v ON_ERROR_STOP=1" should be used - when invoking psql to make sure errors are not suppressed. + The command line flag `-v ON_ERROR_STOP=1` should be used + when invoking `psql` to make sure errors are not suppressed. Example: diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index a9f6aaafef..4daafedcfb 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -27,14 +27,12 @@ module ActiveRecord throw(:abort) end + when :destroy + # No point in executing the counter update since we're going to destroy the parent anyway + load_target.each { |t| t.destroyed_by_association = reflection } + destroy_all else - if options[:dependent] == :destroy - # No point in executing the counter update since we're going to destroy the parent anyway - load_target.each { |t| t.destroyed_by_association = reflection } - destroy_all - else - delete_all - end + delete_all end end diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 1fb5eb28cd..eadd73aab0 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -209,13 +209,13 @@ module ActiveRecord # end # # person = Person.new - # person.respond_to(:name) # => true - # person.respond_to(:name=) # => true - # person.respond_to(:name?) # => true - # person.respond_to('age') # => true - # person.respond_to('age=') # => true - # person.respond_to('age?') # => true - # person.respond_to(:nothing) # => false + # person.respond_to?(:name) # => true + # person.respond_to?(:name=) # => true + # person.respond_to?(:name?) # => true + # person.respond_to?('age') # => true + # person.respond_to?('age=') # => true + # person.respond_to?('age?') # => true + # person.respond_to?(:nothing) # => false def respond_to?(name, include_private = false) return false unless super @@ -279,9 +279,8 @@ module ActiveRecord # Returns an <tt>#inspect</tt>-like string for the value of the # attribute +attr_name+. String attributes are truncated up to 50 # characters, Date and Time attributes are returned in the - # <tt>:db</tt> format, Array attributes are truncated up to 10 values. - # Other attributes return the value of <tt>#inspect</tt> without - # modification. + # <tt>:db</tt> format. Other attributes return the value of + # <tt>#inspect</tt> without modification. # # person = Person.create!(name: 'David Heinemeier Hansson ' * 3) # @@ -292,7 +291,7 @@ module ActiveRecord # # => "\"2012-10-22 00:15:07\"" # # person.attribute_for_inspect(:tag_ids) - # # => "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]" + # # => "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]" def attribute_for_inspect(attr_name) value = read_attribute(attr_name) @@ -300,9 +299,6 @@ module ActiveRecord "#{value[0, 50]}...".inspect elsif value.is_a?(Date) || value.is_a?(Time) %("#{value.to_s(:db)}") - elsif value.is_a?(Array) && value.size > 10 - inspected = value.first(10).inspect - %(#{inspected[0...-1]}, ...]) else value.inspect end diff --git a/activerecord/lib/active_record/attributes.rb b/activerecord/lib/active_record/attributes.rb index 519de271c3..3211b6eaeb 100644 --- a/activerecord/lib/active_record/attributes.rb +++ b/activerecord/lib/active_record/attributes.rb @@ -34,10 +34,10 @@ module ActiveRecord # is not passed, the previous default value (if any) will be used. # Otherwise, the default will be +nil+. # - # +array+ (PG only) specifies that the type should be an array (see the + # +array+ (PostgreSQL only) specifies that the type should be an array (see the # examples below). # - # +range+ (PG only) specifies that the type should be a range (see the + # +range+ (PostgreSQL only) specifies that the type should be a range (see the # examples below). # # ==== Examples 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 c341773be1..9b74c3a10f 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -74,7 +74,7 @@ module ActiveRecord #-- # Synchronization policy: # * all public methods can be called outside +synchronize+ - # * access to these i-vars needs to be in +synchronize+: + # * access to these instance variables needs to be in +synchronize+: # * @connections # * @now_connecting # * private methods that require being called in a +synchronize+ blocks @@ -329,13 +329,13 @@ module ActiveRecord # default max pool size to 5 @size = (spec.config[:pool] && spec.config[:pool].to_i) || 5 - # The cache of threads mapped to reserved connections, the sole purpose - # of the cache is to speed-up +connection+ method, it is not the authoritative - # registry of which thread owns which connection, that is tracked by - # +connection.owner+ attr on each +connection+ instance. + # This variable tracks the cache of threads mapped to reserved connections, with the + # sole purpose of speeding up the +connection+ method. It is not the authoritative + # registry of which thread owns which connection. Connection ownership is tracked by + # the +connection.owner+ attr on each +connection+ instance. # The invariant works like this: if there is mapping of <tt>thread => conn</tt>, - # then that +thread+ does indeed own that +conn+, however an absence of a such - # mapping does not mean that the +thread+ doesn't own the said connection, in + # then that +thread+ does indeed own that +conn+. However, an absence of a such + # mapping does not mean that the +thread+ doesn't own the said connection. In # that case +conn.owner+ attr should be consulted. # Access and modification of +@thread_cached_conns+ does not require # synchronization. @@ -364,10 +364,10 @@ module ActiveRecord @thread_cached_conns[connection_cache_key(Thread.current)] ||= checkout end - # Is there an open connection that is being used for the current thread? + # Returns true if there is an open connection being used for the current thread. # # This method only works for connections that have been obtained through - # #connection or #with_connection methods, connections obtained through + # #connection or #with_connection methods. Connections obtained through # #checkout will not be detected by #active_connection? def active_connection? @thread_cached_conns[connection_cache_key(Thread.current)] @@ -415,7 +415,10 @@ module ActiveRecord with_exclusively_acquired_all_connections(raise_on_acquisition_timeout) do synchronize do @connections.each do |conn| - checkin conn + if conn.in_use? + conn.steal! + checkin conn + end conn.disconnect! end @connections = [] @@ -426,9 +429,9 @@ module ActiveRecord # Disconnects all connections in the pool, and clears the pool. # - # The pool first tries to gain ownership of all connections, if unable to + # The pool first tries to gain ownership of all connections. If unable to # do so within a timeout interval (default duration is - # <tt>spec.config[:checkout_timeout] * 2</tt> seconds), the pool is forcefully + # <tt>spec.config[:checkout_timeout] * 2</tt> seconds), then the pool is forcefully # disconnected without any regard for other connection owning threads. def disconnect! disconnect(false) @@ -447,7 +450,10 @@ module ActiveRecord with_exclusively_acquired_all_connections(raise_on_acquisition_timeout) do synchronize do @connections.each do |conn| - checkin conn + if conn.in_use? + conn.steal! + checkin conn + end conn.disconnect! if conn.requires_reloading? end @connections.delete_if(&:requires_reloading?) @@ -474,9 +480,9 @@ module ActiveRecord # Clears the cache which maps classes and re-connects connections that # require reloading. # - # The pool first tries to gain ownership of all connections, if unable to + # The pool first tries to gain ownership of all connections. If unable to # do so within a timeout interval (default duration is - # <tt>spec.config[:checkout_timeout] * 2</tt> seconds), the pool forcefully + # <tt>spec.config[:checkout_timeout] * 2</tt> seconds), then the pool forcefully # clears the cache and reloads connections without any regard for other # connection owning threads. def clear_reloadable_connections! @@ -530,20 +536,20 @@ module ActiveRecord @available.delete conn # @available.any_waiting? => true means that prior to removing this - # conn, the pool was at its max size (@connections.size == @size) - # this would mean that any threads stuck waiting in the queue wouldn't + # conn, the pool was at its max size (@connections.size == @size). + # This would mean that any threads stuck waiting in the queue wouldn't # know they could checkout_new_connection, so let's do it for them. # Because condition-wait loop is encapsulated in the Queue class # (that in turn is oblivious to ConnectionPool implementation), threads - # that are "stuck" there are helpless, they have no way of creating + # that are "stuck" there are helpless. They have no way of creating # new connections and are completely reliant on us feeding available # connections into the Queue. needs_new_connection = @available.any_waiting? end # This is intentionally done outside of the synchronized section as we - # would like not to hold the main mutex while checking out new connections, - # thus there is some chance that needs_new_connection information is now + # would like not to hold the main mutex while checking out new connections. + # Thus there is some chance that needs_new_connection information is now # stale, we can live with that (bulk_make_new_connections will make # sure not to exceed the pool's @size limit). bulk_make_new_connections(1) if needs_new_connection @@ -556,17 +562,17 @@ module ActiveRecord stale_connections = synchronize do @connections.select do |conn| conn.in_use? && !conn.owner.alive? + end.each do |conn| + conn.steal! end end stale_connections.each do |conn| - synchronize do - if conn.active? - conn.reset! - checkin conn - else - remove conn - end + if conn.active? + conn.reset! + checkin conn + else + remove conn end end end @@ -698,7 +704,7 @@ module ActiveRecord def acquire_connection(checkout_timeout) # NOTE: we rely on +@available.poll+ and +try_to_checkout_new_connection+ to # +conn.lease+ the returned connection (and to do this in a +synchronized+ - # section), this is not the cleanest implementation, as ideally we would + # section). This is not the cleanest implementation, as ideally we would # <tt>synchronize { conn.lease }</tt> in this method, but by leaving it to +@available.poll+ # and +try_to_checkout_new_connection+ we can piggyback on +synchronize+ sections # of the said methods and avoid an additional +synchronize+ overhead. @@ -822,7 +828,7 @@ module ActiveRecord # should use. # # The ConnectionHandler class is not coupled with the Active models, as it has no knowlodge - # about the model. The model, needs to pass a specification name to the handler, + # about the model. The model needs to pass a specification name to the handler, # in order to lookup the correct connection pool. class ConnectionHandler def initialize @@ -890,7 +896,7 @@ module ActiveRecord # Remove the connection for this class. This will close the active # connection and the defined connection (if they exist). The result - # can be used as an argument for establish_connection, for easily + # can be used as an argument for #establish_connection, for easily # re-establishing the connection. def remove_connection(spec_name) if pool = owner_to_pool.delete(spec_name) 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 eec0bc8518..396cb0b07a 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -179,7 +179,7 @@ module ActiveRecord # A Symbol can be used to specify the type of the generated primary key column. # [<tt>:primary_key</tt>] # The name of the primary key, if one is to be added automatically. - # Defaults to +id+. If <tt>:id</tt> is false this option is ignored. + # Defaults to +id+. If <tt>:id</tt> is false, then this option is ignored. # # Note that Active Record models will automatically detect their # primary key. This can be avoided by using @@ -305,9 +305,9 @@ module ActiveRecord # # Creates a table called 'assemblies_parts' with no id. # create_join_table(:assemblies, :parts) # - # You can pass a +options+ hash can include the following keys: + # You can pass an +options+ hash which can include the following keys: # [<tt>:table_name</tt>] - # Sets the table name overriding the default + # Sets the table name, overriding the default. # [<tt>:column_options</tt>] # Any extra options you want appended to the columns definition. # [<tt>:options</tt>] @@ -433,7 +433,7 @@ module ActiveRecord # t.remove_index :company_id # end # - # See also Table for details on all of the various column transformation. + # See also Table for details on all of the various column transformations. def change_table(table_name, options = {}) if supports_bulk_alter? && options[:bulk] recorder = ActiveRecord::Migration::CommandRecorder.new(self) @@ -483,10 +483,10 @@ module ActiveRecord # # Available options are (none of these exists by default): # * <tt>:limit</tt> - - # Requests a maximum column length. This is number of characters for a <tt>:string</tt> column + # Requests a maximum column length. This is the number of characters for a <tt>:string</tt> column # and number of bytes for <tt>:text</tt>, <tt>:binary</tt> and <tt>:integer</tt> columns. # * <tt>:default</tt> - - # The column's default value. Use nil for NULL. + # The column's default value. Use +nil+ for +NULL+. # * <tt>:null</tt> - # Allows or disallows +NULL+ values in the column. This option could # have been named <tt>:null_allowed</tt>. @@ -495,7 +495,7 @@ module ActiveRecord # * <tt>:scale</tt> - # Specifies the scale for the <tt>:decimal</tt> and <tt>:numeric</tt> columns. # - # Note: The precision is the total number of significant digits + # Note: The precision is the total number of significant digits, # and the scale is the number of digits that can be stored following # the decimal point. For example, the number 123.45 has a precision of 5 # and a scale of 2. A decimal with a precision of 5 and a scale of 2 can @@ -564,7 +564,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. + # In that case, +type+ and +options+ will be used by #add_column. def remove_column(table_name, column_name, type = nil, options = {}) execute "ALTER TABLE #{quote_table_name(table_name)} DROP #{quote_column_name(column_name)}" end @@ -952,13 +952,13 @@ module ActiveRecord # Checks to see if a foreign key exists on a table for a given foreign key definition. # - # # Check a foreign key exists + # # Checks to see if a foreign key exists. # foreign_key_exists?(:accounts, :branches) # - # # Check a foreign key on a specified column exists + # # Checks to see if a foreign key on a specified column exists. # foreign_key_exists?(:accounts, column: :owner_id) # - # # Check a foreign key with a custom name exists + # # Checks to see if a foreign key with a custom name exists. # foreign_key_exists?(:accounts, name: "special_fk_name") # def foreign_key_exists?(from_table, options_or_to_table = {}) @@ -1081,7 +1081,7 @@ module ActiveRecord end # Given a set of columns and an ORDER BY clause, returns the columns for a SELECT DISTINCT. - # PostgreSQL, MySQL, and Oracle overrides this for custom DISTINCT syntax - they + # PostgreSQL, MySQL, and Oracle override this for custom DISTINCT syntax - they # require the order columns appear in the SELECT. # # columns_for_distinct("posts.id", ["posts.created_at desc"]) diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index d4b9e301bc..5747e4d1ee 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -184,7 +184,30 @@ module ActiveRecord # this method must only be called while holding connection pool's mutex def expire - @owner = nil + if in_use? + if @owner != Thread.current + raise ActiveRecordError, "Cannot expire connection, " << + "it is owned by a different thread: #{@owner}. " << + "Current thread: #{Thread.current}." + end + + @owner = nil + else + raise ActiveRecordError, 'Cannot expire connection, it is not currently leased.' + end + end + + # this method must only be called while holding connection pool's mutex (and a desire for segfaults) + def steal! # :nodoc: + if in_use? + if @owner != Thread.current + pool.send :remove_connection_from_thread_cache, self, @owner + + @owner = Thread.current + end + else + raise ActiveRecordError, 'Cannot steal connection, it is not currently leased.' + end end def unprepared_statement 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 718a6c5b91..3e77b92141 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -460,7 +460,6 @@ module ActiveRecord # it can be helpful to provide these in a migration's +change+ method so it can be reverted. # In that case, +options+ and the block will be used by create_table. def drop_table(table_name, options = {}) - create_table_info_cache.delete(table_name) if create_table_info_cache.key?(table_name) execute "DROP#{' TEMPORARY' if options[:temporary]} TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}#{' CASCADE' if options[:force] == :cascade}" end @@ -515,19 +514,21 @@ module ActiveRecord schema, name = extract_schema_qualified_name(table_name) - fk_info = select_all <<-SQL.strip_heredoc - SELECT fk.referenced_table_name as 'to_table' - ,fk.referenced_column_name as 'primary_key' - ,fk.column_name as 'column' - ,fk.constraint_name as 'name' + fk_info = select_all(<<-SQL.strip_heredoc, 'SCHEMA') + SELECT fk.referenced_table_name AS 'to_table', + fk.referenced_column_name AS 'primary_key', + fk.column_name AS 'column', + fk.constraint_name AS 'name', + rc.update_rule AS 'on_update', + rc.delete_rule AS 'on_delete' FROM information_schema.key_column_usage fk - WHERE fk.referenced_column_name is not null + JOIN information_schema.referential_constraints rc + USING (constraint_schema, constraint_name) + WHERE fk.referenced_column_name IS NOT NULL AND fk.table_schema = #{quote(schema)} AND fk.table_name = #{quote(name)} SQL - create_table_info = create_table_info(table_name) - fk_info.map do |row| options = { column: row['column'], @@ -535,8 +536,8 @@ module ActiveRecord primary_key: row['primary_key'] } - options[:on_update] = extract_foreign_key_action(create_table_info, row['name'], "UPDATE") - options[:on_delete] = extract_foreign_key_action(create_table_info, row['name'], "DELETE") + options[:on_update] = extract_foreign_key_action(row['on_update']) + options[:on_delete] = extract_foreign_key_action(row['on_delete']) ForeignKeyDefinition.new(table_name, row['to_table'], options) end @@ -891,21 +892,15 @@ module ActiveRecord end end - def extract_foreign_key_action(structure, name, action) # :nodoc: - if structure =~ /CONSTRAINT #{quote_column_name(name)} FOREIGN KEY .* REFERENCES .* ON #{action} (CASCADE|SET NULL|RESTRICT)/ - case $1 - when 'CASCADE'; :cascade - when 'SET NULL'; :nullify - end + def extract_foreign_key_action(specifier) # :nodoc: + case specifier + when 'CASCADE'; :cascade + when 'SET NULL'; :nullify end end - def create_table_info_cache # :nodoc: - @create_table_info_cache ||= {} - end - def create_table_info(table_name) # :nodoc: - create_table_info_cache[table_name] ||= select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"] + select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"] end def create_table_definition(*args) # :nodoc: 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 13c9b6cbd9..87f0ff7d85 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb @@ -13,19 +13,6 @@ module ActiveRecord result end - # Returns a record hash with the column names as keys and column values - # as values. - def select_one(arel, name = nil, binds = []) - arel, binds = binds_from_relation(arel, binds) - @connection.query_options.merge!(as: :hash) - select_result(to_sql(arel, binds), name, binds) do |result| - @connection.next_result while @connection.more_results? - result.first - end - ensure - @connection.query_options.merge!(as: :array) - end - # Returns an array of arrays containing the field values. # Order is the same as that returned by +columns+. def select_rows(sql, name = nil, binds = []) 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 f6860b9aba..45507e206a 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -579,7 +579,7 @@ module ActiveRecord end def foreign_keys(table_name) - fk_info = select_all <<-SQL.strip_heredoc + fk_info = select_all(<<-SQL.strip_heredoc, 'SCHEMA') SELECT t2.oid::regclass::text AS to_table, a1.attname AS column, a2.attname AS primary_key, c.conname AS name, c.confupdtype AS on_update, c.confdeltype AS on_delete FROM pg_constraint c JOIN pg_class t1 ON c.conrelid = t1.oid diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb index 99fdef67d0..f735bc697b 100644 --- a/activerecord/lib/active_record/connection_handling.rb +++ b/activerecord/lib/active_record/connection_handling.rb @@ -98,14 +98,6 @@ module ActiveRecord @connection_specification_name end - def connection_id - ActiveRecord::RuntimeRegistry.connection_id ||= Thread.current.object_id - end - - def connection_id=(connection_id) - ActiveRecord::RuntimeRegistry.connection_id = connection_id - end - # Returns the configuration of the associated connection as a hash: # # ActiveRecord::Base.connection_config diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb index 466c8509a4..d729deb07b 100644 --- a/activerecord/lib/active_record/integration.rb +++ b/activerecord/lib/active_record/integration.rb @@ -86,7 +86,7 @@ module ActiveRecord # # user = User.find_by(name: 'David Heinemeier Hansson') # user.id # => 125 - # user_path(user) # => "/users/125-david" + # user_path(user) # => "/users/125-david-heinemeier" # # Because the generated param begins with the record's +id+, it is # suitable for passing to +find+. In a controller, for example: @@ -100,7 +100,7 @@ module ActiveRecord define_method :to_param do if (default = super()) && (result = send(method_name).to_s).present? && - (param = result.squish.truncate(20, separator: /\s/, omission: nil).parameterize).present? + (param = result.squish.parameterize.truncate(20, separator: /-/, omission: '')).present? "#{default}-#{param}" else default diff --git a/activerecord/lib/active_record/internal_metadata.rb b/activerecord/lib/active_record/internal_metadata.rb index 17a5dc1d1b..8b649eab0f 100644 --- a/activerecord/lib/active_record/internal_metadata.rb +++ b/activerecord/lib/active_record/internal_metadata.rb @@ -14,10 +14,6 @@ module ActiveRecord "#{table_name_prefix}#{ActiveRecord::Base.internal_metadata_table_name}#{table_name_suffix}" end - def original_table_name - "#{table_name_prefix}active_record_internal_metadatas#{table_name_suffix}" - end - def []=(key, value) find_or_initialize_by(key: key).update_attributes!(value: value) end @@ -30,17 +26,8 @@ module ActiveRecord ActiveSupport::Deprecation.silence { connection.table_exists?(table_name) } end - def original_table_exists? - # This method will be removed in Rails 5.1 - # Since it is only necessary when `active_record_internal_metadatas` could exist - ActiveSupport::Deprecation.silence { connection.table_exists?(original_table_name) } - end - # Creates an internal metadata table with columns +key+ and +value+ def create_table - if original_table_exists? - connection.rename_table(original_table_name, table_name) - end unless table_exists? key_options = connection.internal_string_options_for_primary_key diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index afed5e5e85..510e8a6e43 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -479,7 +479,12 @@ module ActiveRecord # ball.touch(:updated_at) # => raises ActiveRecordError # def touch(*names, time: nil) - raise ActiveRecordError, "cannot touch on a new record object" unless persisted? + unless persisted? + raise ActiveRecordError, <<-MSG.squish + cannot touch on a new or destroyed record object. Consider using + persisted?, new_record?, or destroyed? before touching + MSG + end time ||= current_time_from_proper_timezone attributes = timestamp_attributes_for_update_in_model diff --git a/activerecord/lib/active_record/query_cache.rb b/activerecord/lib/active_record/query_cache.rb index 07d863d15e..387dd8e9bd 100644 --- a/activerecord/lib/active_record/query_cache.rb +++ b/activerecord/lib/active_record/query_cache.rb @@ -26,16 +26,12 @@ module ActiveRecord def self.run connection = ActiveRecord::Base.connection enabled = connection.query_cache_enabled - connection_id = ActiveRecord::Base.connection_id connection.enable_query_cache! - [enabled, connection_id] + enabled end - def self.complete(state) - enabled, connection_id = state - - ActiveRecord::Base.connection_id = connection_id + def self.complete(enabled) ActiveRecord::Base.connection.clear_query_cache ActiveRecord::Base.connection.disable_query_cache! unless enabled end @@ -44,7 +40,7 @@ module ActiveRecord executor.register_hook(self) executor.to_complete do - unless ActiveRecord::Base.connection.transaction_open? + unless ActiveRecord::Base.connected? && ActiveRecord::Base.connection.transaction_open? ActiveRecord::Base.clear_active_connections! end end diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 7a1552856b..93baa882ad 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -65,7 +65,7 @@ module ActiveRecord @klass.connection.insert( im, 'SQL', - primary_key, + primary_key || false, primary_key_value, nil, binds) diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index ecce949370..e5496c02b2 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -15,11 +15,11 @@ module ActiveRecord @table = table @handlers = [] - register_handler(BasicObject, BasicObjectHandler.new(self)) + register_handler(BasicObject, BasicObjectHandler.new) register_handler(Class, ClassHandler.new(self)) register_handler(Base, BaseHandler.new(self)) - register_handler(Range, RangeHandler.new(self)) - register_handler(RangeHandler::RangeWithBinds, RangeHandler.new(self)) + register_handler(Range, RangeHandler.new) + register_handler(RangeHandler::RangeWithBinds, RangeHandler.new) register_handler(Relation, RelationHandler.new) register_handler(Array, ArrayHandler.new(self)) register_handler(AssociationQueryValue, AssociationQueryHandler.new(self)) diff --git a/activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb index 6cec75dc0a..79cde00303 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb @@ -1,17 +1,9 @@ module ActiveRecord class PredicateBuilder class BasicObjectHandler # :nodoc: - def initialize(predicate_builder) - @predicate_builder = predicate_builder - end - def call(attribute, value) attribute.eq(value) end - - protected - - attr_reader :predicate_builder end end end diff --git a/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb index 306d4694ae..5db778e19c 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb @@ -3,10 +3,6 @@ module ActiveRecord class RangeHandler # :nodoc: RangeWithBinds = Struct.new(:begin, :end, :exclude_end?) - def initialize(predicate_builder) - @predicate_builder = predicate_builder - end - def call(attribute, value) if value.begin.respond_to?(:infinite?) && value.begin.infinite? if value.end.respond_to?(:infinite?) && value.end.infinite? @@ -24,10 +20,6 @@ module ActiveRecord attribute.between(value) end end - - protected - - attr_reader :predicate_builder end end end diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb index 8e6cd6c82f..b9fd0f5326 100644 --- a/activerecord/lib/active_record/result.rb +++ b/activerecord/lib/active_record/result.rb @@ -75,8 +75,14 @@ module ActiveRecord hash_rows[idx] end + def first + return nil if @rows.empty? + Hash[@columns.zip(@rows.first)] + end + def last - hash_rows.last + return nil if @rows.empty? + Hash[@columns.zip(@rows.last)] end def cast_values(type_overrides = {}) # :nodoc: diff --git a/activerecord/lib/active_record/runtime_registry.rb b/activerecord/lib/active_record/runtime_registry.rb index 56e88bc661..9e86999c1b 100644 --- a/activerecord/lib/active_record/runtime_registry.rb +++ b/activerecord/lib/active_record/runtime_registry.rb @@ -12,9 +12,9 @@ module ActiveRecord class RuntimeRegistry # :nodoc: extend ActiveSupport::PerThreadRegistry - attr_accessor :connection_handler, :sql_runtime, :connection_id + attr_accessor :connection_handler, :sql_runtime - [:connection_handler, :sql_runtime, :connection_id].each do |val| + [:connection_handler, :sql_runtime].each do |val| class_eval %{ def self.#{val}; instance.#{val}; end }, __FILE__, __LINE__ class_eval %{ def self.#{val}=(x); instance.#{val}=x; end }, __FILE__, __LINE__ end diff --git a/activerecord/lib/active_record/touch_later.rb b/activerecord/lib/active_record/touch_later.rb index 9a80a63e28..598873ea3a 100644 --- a/activerecord/lib/active_record/touch_later.rb +++ b/activerecord/lib/active_record/touch_later.rb @@ -8,7 +8,12 @@ module ActiveRecord end def touch_later(*names) # :nodoc: - raise ActiveRecordError, "cannot touch on a new record object" unless persisted? + unless persisted? + raise ActiveRecordError, <<-MSG.squish + cannot touch on a new or destroyed record object. Consider using + persisted?, new_record?, or destroyed? before touching + MSG + end @_defer_touch_attrs ||= timestamp_attributes_for_update_in_model @_defer_touch_attrs |= names diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 77c2845d88..0ee1ebcfbe 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -189,8 +189,8 @@ module ActiveRecord # # === Caveats # - # If you're on MySQL, then do not use DDL operations in nested transactions - # blocks that are emulated with savepoints. That is, do not execute statements + # If you're on MySQL, then do not use Data Definition Language(DDL) operations in nested + # transactions blocks that are emulated with savepoints. That is, do not execute statements # like 'CREATE TABLE' inside such blocks. This is because MySQL automatically # releases all savepoints upon executing a DDL operation. When +transaction+ # is finished and tries to release the savepoint it created earlier, a @@ -480,11 +480,11 @@ module ActiveRecord # Updates the attributes on this particular Active Record object so that # if it's associated with a transaction, then the state of the Active Record - # object will be updated to reflect the current state of the transaction + # object will be updated to reflect the current state of the transaction. # # The +@transaction_state+ variable stores the states of the associated # transaction. This relies on the fact that a transaction can only be in - # one rollback or commit (otherwise a list of states would be required) + # one rollback or commit (otherwise a list of states would be required). # Each Active Record object inside of a transaction carries that transaction's # TransactionState. # diff --git a/activerecord/test/cases/adapters/mysql2/schema_test.rb b/activerecord/test/cases/adapters/mysql2/schema_test.rb index 43957791b1..57ea8258d1 100644 --- a/activerecord/test/cases/adapters/mysql2/schema_test.rb +++ b/activerecord/test/cases/adapters/mysql2/schema_test.rb @@ -103,3 +103,24 @@ module ActiveRecord end end end + +class Mysql2AnsiQuotesTest < ActiveRecord::Mysql2TestCase + def setup + @connection = ActiveRecord::Base.connection + @connection.execute("SET SESSION sql_mode='ANSI_QUOTES'") + end + + def teardown + @connection.reconnect! + end + + def test_primary_key_method_with_ansi_quotes + assert_equal "id", @connection.primary_key("topics") + end + + def test_foreign_keys_method_with_ansi_quotes + fks = @connection.foreign_keys("lessons_students") + assert_equal([["lessons_students", "students", :cascade]], + fks.map {|fk| [fk.from_table, fk.to_table, fk.on_delete] }) + end +end diff --git a/activerecord/test/cases/adapters/postgresql/array_test.rb b/activerecord/test/cases/adapters/postgresql/array_test.rb index 380a90d765..70792e937c 100644 --- a/activerecord/test/cases/adapters/postgresql/array_test.rb +++ b/activerecord/test/cases/adapters/postgresql/array_test.rb @@ -189,8 +189,13 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase end def test_attribute_for_inspect_for_array_field + record = PgArray.new { |a| a.ratings = (1..10).to_a } + assert_equal("[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]", record.attribute_for_inspect(:ratings)) + end + + def test_attribute_for_inspect_for_array_field_for_large_array record = PgArray.new { |a| a.ratings = (1..11).to_a } - assert_equal("[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]", record.attribute_for_inspect(:ratings)) + assert_equal("[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]", record.attribute_for_inspect(:ratings)) end def test_escaping diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index 1db52af59b..04126e87e4 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -27,14 +27,33 @@ class AttributeMethodsTest < ActiveRecord::TestCase ActiveRecord::Base.send(:attribute_method_matchers).concat(@old_matchers) end - def test_attribute_for_inspect + def test_attribute_for_inspect_string t = topics(:first) t.title = "The First Topic Now Has A Title With\nNewlines And More Than 50 Characters" - assert_equal %("#{t.written_on.to_s(:db)}"), t.attribute_for_inspect(:written_on) assert_equal '"The First Topic Now Has A Title With\nNewlines And ..."', t.attribute_for_inspect(:title) end + def test_attribute_for_inspect_date + t = topics(:first) + + assert_equal %("#{t.written_on.to_s(:db)}"), t.attribute_for_inspect(:written_on) + end + + def test_attribute_for_inspect_array + t = topics(:first) + t.content = [Object.new] + + assert_match %r(\[#<Object:0x[0-9a-f]+>\]), t.attribute_for_inspect(:content) + end + + def test_attribute_for_inspect_long_array + t = topics(:first) + t.content = (1..11).to_a + + assert_equal "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]", t.attribute_for_inspect(:content) + end + def test_attribute_present t = Topic.new t.title = "hello there!" diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb index a45ee281c7..09e7848bda 100644 --- a/activerecord/test/cases/connection_pool_test.rb +++ b/activerecord/test/cases/connection_pool_test.rb @@ -151,7 +151,7 @@ module ActiveRecord assert_equal 1, active_connections(@pool).size ensure - @pool.connections.each(&:close) + @pool.connections.each { |conn| conn.close if conn.in_use? } end def test_remove_connection @@ -432,6 +432,9 @@ module ActiveRecord Thread.new { @pool.send(group_action_method) }.join # assert connection has been forcefully taken away from us assert_not @pool.active_connection? + + # make a new connection for with_connection to clean up + @pool.connection end end end diff --git a/activerecord/test/cases/invalid_date_test.rb b/activerecord/test/cases/date_test.rb index 426a350379..f112081c14 100644 --- a/activerecord/test/cases/invalid_date_test.rb +++ b/activerecord/test/cases/date_test.rb @@ -1,7 +1,19 @@ require 'cases/helper' require 'models/topic' -class InvalidDateTest < ActiveRecord::TestCase +class DateTest < ActiveRecord::TestCase + def test_date_with_time_value + time_value = Time.new(2016, 05, 11, 19, 0, 0) + topic = Topic.create(last_read: time_value) + assert_equal topic, Topic.find_by(last_read: time_value) + end + + def test_date_with_string_value + string_value = '2016-05-11 19:00:00' + topic = Topic.create(last_read: string_value) + assert_equal topic, Topic.find_by(last_read: string_value) + end + def test_assign_valid_dates valid_dates = [[2007, 11, 30], [1993, 2, 28], [2008, 2, 29]] diff --git a/activerecord/test/cases/integration_test.rb b/activerecord/test/cases/integration_test.rb index 08a186ae07..97a0cdb5db 100644 --- a/activerecord/test/cases/integration_test.rb +++ b/activerecord/test/cases/integration_test.rb @@ -29,10 +29,30 @@ class IntegrationTest < ActiveRecord::TestCase assert_equal '4-flamboyant-software', firm.to_param end + def test_to_param_class_method_truncates_words_properly + firm = Firm.find(4) + firm.name << ', Inc.' + assert_equal '4-flamboyant-software', firm.to_param + end + + def test_to_param_class_method_truncates_after_parameterize + firm = Firm.find(4) + firm.name = "Huey, Dewey, & Louie LLC" + # 123456789T123456789v + assert_equal '4-huey-dewey-louie-llc', firm.to_param + end + + def test_to_param_class_method_truncates_after_parameterize_with_hyphens + firm = Firm.find(4) + firm.name = "Door-to-Door Wash-n-Fold Service" + # 123456789T123456789v + assert_equal '4-door-to-door-wash-n', firm.to_param + end + def test_to_param_class_method_truncates firm = Firm.find(4) firm.name = 'a ' * 100 - assert_equal '4-a-a-a-a-a-a-a-a-a', firm.to_param + assert_equal '4-a-a-a-a-a-a-a-a-a-a', firm.to_param end def test_to_param_class_method_truncates_edge_case @@ -41,10 +61,16 @@ class IntegrationTest < ActiveRecord::TestCase assert_equal '4-david', firm.to_param end + def test_to_param_class_method_truncates_case_shown_in_doc + firm = Firm.find(4) + firm.name = 'David Heinemeier Hansson' + assert_equal '4-david-heinemeier', firm.to_param + end + def test_to_param_class_method_squishes firm = Firm.find(4) firm.name = "ab \n" * 100 - assert_equal '4-ab-ab-ab-ab-ab-ab', firm.to_param + assert_equal '4-ab-ab-ab-ab-ab-ab-ab', firm.to_param end def test_to_param_class_method_multibyte_character diff --git a/activerecord/test/cases/migration/foreign_key_test.rb b/activerecord/test/cases/migration/foreign_key_test.rb index 01162dcefe..4237e89731 100644 --- a/activerecord/test/cases/migration/foreign_key_test.rb +++ b/activerecord/test/cases/migration/foreign_key_test.rb @@ -232,6 +232,10 @@ module ActiveRecord t.column :city_id, :integer end add_foreign_key :houses, :cities, column: "city_id" + + # remove and re-add to test that schema is updated and not accidentally cached + remove_foreign_key :houses, :cities + add_foreign_key :houses, :cities, column: "city_id", on_delete: :cascade end end @@ -243,6 +247,15 @@ module ActiveRecord silence_stream($stdout) { migration.migrate(:down) } end + def test_foreign_key_constraint_is_not_cached_incorrectly + migration = CreateCitiesAndHousesMigration.new + silence_stream($stdout) { migration.migrate(:up) } + output = dump_table_schema "houses" + assert_match %r{\s+add_foreign_key "houses",.+on_delete: :cascade$}, output + ensure + silence_stream($stdout) { migration.migrate(:down) } + end + class CreateSchoolsAndClassesMigration < ActiveRecord::Migration::Current def change create_table(:schools) diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index 36b6662820..d499aadbae 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -445,21 +445,6 @@ class MigrationTest < ActiveRecord::TestCase ActiveRecord::Migrator.migrations_paths = old_path end - def test_rename_internal_metadata_table - original_internal_metadata_table_name = ActiveRecord::Base.internal_metadata_table_name - - ActiveRecord::Base.internal_metadata_table_name = "active_record_internal_metadatas" - Reminder.reset_table_name - - ActiveRecord::Base.internal_metadata_table_name = original_internal_metadata_table_name - Reminder.reset_table_name - - assert_equal "ar_internal_metadata", ActiveRecord::InternalMetadata.table_name - ensure - ActiveRecord::Base.internal_metadata_table_name = original_internal_metadata_table_name - Reminder.reset_table_name - end - def test_proper_table_name_on_migration reminder_class = new_isolated_reminder_class migration = ActiveRecord::Migration.new diff --git a/activerecord/test/cases/mixin_test.rb b/activerecord/test/cases/mixin_test.rb index 7ebdcac711..06af75af0f 100644 --- a/activerecord/test/cases/mixin_test.rb +++ b/activerecord/test/cases/mixin_test.rb @@ -41,13 +41,12 @@ class TouchTest < ActiveRecord::TestCase old_updated_at = stamped.updated_at - travel 5.minutes do - stamped.lft_will_change! - stamped.save + travel 5.minutes + stamped.lft_will_change! + stamped.save - assert_equal Time.now, stamped.updated_at - assert_equal old_updated_at, stamped.created_at - end + assert_equal Time.now, stamped.updated_at + assert_equal old_updated_at, stamped.created_at end def test_create_turned_off diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb index 52eac4a124..4267ad4a24 100644 --- a/activerecord/test/cases/primary_keys_test.rb +++ b/activerecord/test/cases/primary_keys_test.rb @@ -174,6 +174,14 @@ class PrimaryKeysTest < ActiveRecord::TestCase assert_equal '2', dashboard.id end + def test_create_without_primary_key_no_extra_query + klass = Class.new(ActiveRecord::Base) do + self.table_name = 'dashboards' + end + klass.create! # warmup schema cache + assert_queries(3, ignore_none: true) { klass.create! } + end + if current_adapter?(:PostgreSQLAdapter) def test_serial_with_quoted_sequence_name column = MixedCaseMonkey.columns_hash[MixedCaseMonkey.primary_key] @@ -275,18 +283,6 @@ class CompositePrimaryKeyTest < ActiveRecord::TestCase end if current_adapter?(:Mysql2Adapter) - class PrimaryKeyWithAnsiQuotesTest < ActiveRecord::TestCase - self.use_transactional_tests = false - - def test_primary_key_method_with_ansi_quotes - con = ActiveRecord::Base.connection - con.execute("SET SESSION sql_mode='ANSI_QUOTES'") - assert_equal "id", con.primary_key("topics") - ensure - con.reconnect! - end - end - class PrimaryKeyBigintNilDefaultTest < ActiveRecord::TestCase include SchemaDumpingHelper diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb index d5c01315c1..03ec063671 100644 --- a/activerecord/test/cases/query_cache_test.rb +++ b/activerecord/test/cases/query_cache_test.rb @@ -39,18 +39,6 @@ class QueryCacheTest < ActiveRecord::TestCase assert ActiveRecord::Base.connection.query_cache_enabled, 'cache on' end - def test_exceptional_middleware_assigns_original_connection_id_on_error - connection_id = ActiveRecord::Base.connection_id - - mw = middleware { |env| - ActiveRecord::Base.connection_id = self.object_id - raise "lol borked" - } - assert_raises(RuntimeError) { mw.call({}) } - - assert_equal connection_id, ActiveRecord::Base.connection_id - end - def test_middleware_delegates called = false mw = middleware { |env| diff --git a/activerecord/test/cases/result_test.rb b/activerecord/test/cases/result_test.rb index dec01dfa76..83dc5347e0 100644 --- a/activerecord/test/cases/result_test.rb +++ b/activerecord/test/cases/result_test.rb @@ -22,6 +22,16 @@ module ActiveRecord ], result.to_hash end + test "first returns first row as a hash" do + assert_equal( + {'col_1' => 'row 1 col 1', 'col_2' => 'row 1 col 2'}, result.first) + end + + test "last returns last row as a hash" do + assert_equal( + {'col_1' => 'row 3 col 1', 'col_2' => 'row 3 col 2'}, result.last) + end + test "each with block returns row hashes" do result.each do |row| assert_equal ['col_1', 'col_2'], row.keys diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 628a59c2e3..2f2993ce18 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -409,6 +409,14 @@ ActiveRecord::Schema.define do t.references :student end + create_table :students, force: true do |t| + t.string :name + t.boolean :active + t.integer :college_id + end + + add_foreign_key :lessons_students, :students, on_delete: :cascade + create_table :lint_models, force: true create_table :line_items, force: true do |t| @@ -777,12 +785,6 @@ ActiveRecord::Schema.define do t.integer :lock_version, null: false, default: 0 end - create_table :students, force: true do |t| - t.string :name - t.boolean :active - t.integer :college_id - end - create_table :subscribers, force: true, id: false do |t| t.string :nick, null: false t.string :name @@ -1002,7 +1004,6 @@ ActiveRecord::Schema.define do end add_foreign_key :fk_test_has_fk, :fk_test_has_pk, column: "fk_id", name: "fk_name", primary_key: "pk_id" - add_foreign_key :lessons_students, :students end create_table :overloaded_types, force: true do |t| |