diff options
Diffstat (limited to 'activerecord/lib/active_record')
25 files changed, 300 insertions, 173 deletions
diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index e6a45487d0..02f45731c9 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -226,7 +226,12 @@ module ActiveRecord # Returns true if inverse association on the given record needs to be set. # This method is redefined by subclasses. def invertible_for?(record) - inverse_reflection_for(record) + foreign_key_for?(record) && inverse_reflection_for(record) + end + + # Returns true if record contains the foreign_key + def foreign_key_for?(record) + record.attributes.has_key? reflection.foreign_key end # This should be implemented to return the values of the relevant key(s) on the owner, diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb index 56331bbb0b..31b8d27892 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -163,7 +163,7 @@ module ActiveRecord delete_through_records(records) - if source_reflection.options[:counter_cache] + if source_reflection.options[:counter_cache] && method != :destroy counter = source_reflection.counter_cache_column klass.decrement_counter counter, records.map(&:id) end diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 0008600418..944caacab6 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -26,11 +26,13 @@ module ActiveRecord load_target return self.target if !(target || record) - if (target != record) || record.changed? + + assigning_another_record = target != record + if assigning_another_record || record.changed? save &&= owner.persisted? transaction_if(save) do - remove_target!(options[:dependent]) if target && !target.destroyed? + remove_target!(options[:dependent]) if target && !target.destroyed? && assigning_another_record if record set_owner_attributes(record) diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 69a9eabefb..e05e22ebb0 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -295,6 +295,7 @@ module ActiveRecord #:nodoc: extend Delegation::DelegateCache include Persistence + include NoTouching include ReadonlyAttributes include ModelSchema include Inheritance diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb new file mode 100644 index 0000000000..7c330a2f25 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb @@ -0,0 +1,83 @@ +module ActiveRecord + module ConnectionAdapters + class AbstractAdapter + class SchemaCreation # :nodoc: + def initialize(conn) + @conn = conn + @cache = {} + end + + def accept(o) + m = @cache[o.class] ||= "visit_#{o.class.name.split('::').last}" + send m, o + end + + def visit_AddColumn(o) + sql_type = type_to_sql(o.type.to_sym, o.limit, o.precision, o.scale) + sql = "ADD #{quote_column_name(o.name)} #{sql_type}" + add_column_options!(sql, column_options(o)) + end + + private + + def visit_AlterTable(o) + sql = "ALTER TABLE #{quote_table_name(o.name)} " + sql << o.adds.map { |col| visit_AddColumn col }.join(' ') + end + + def visit_ColumnDefinition(o) + sql_type = type_to_sql(o.type.to_sym, o.limit, o.precision, o.scale) + column_sql = "#{quote_column_name(o.name)} #{sql_type}" + add_column_options!(column_sql, column_options(o)) unless o.primary_key? + column_sql + end + + def visit_TableDefinition(o) + create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE " + create_sql << "#{quote_table_name(o.name)} (" + create_sql << o.columns.map { |c| accept c }.join(', ') + create_sql << ") #{o.options}" + create_sql + end + + def column_options(o) + column_options = {} + column_options[:null] = o.null unless o.null.nil? + column_options[:default] = o.default unless o.default.nil? + column_options[:column] = o + column_options[:first] = o.first + column_options[:after] = o.after + column_options + end + + def quote_column_name(name) + @conn.quote_column_name name + end + + def quote_table_name(name) + @conn.quote_table_name name + end + + def type_to_sql(type, limit, precision, scale) + @conn.type_to_sql type.to_sym, limit, precision, scale + end + + def add_column_options!(sql, options) + sql << " DEFAULT #{@conn.quote(options[:default], options[:column])}" if options_include_default?(options) + # must explicitly check for :null to allow change_column to work on migrations + if options[:null] == false + sql << " NOT NULL" + end + if options[:auto_increment] == true + sql << " AUTO_INCREMENT" + end + sql + end + + def options_include_default?(options) + options.include?(:default) && !(options[:null] == false && options[:default].nil?) + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index cbe563676b..8aa1ce5c04 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -4,6 +4,7 @@ require 'bigdecimal/util' require 'active_support/core_ext/benchmark' require 'active_record/connection_adapters/schema_cache' require 'active_record/connection_adapters/abstract/schema_dumper' +require 'active_record/connection_adapters/abstract/schema_creation' require 'monitor' module ActiveRecord @@ -16,6 +17,7 @@ module ActiveRecord autoload_at 'active_record/connection_adapters/abstract/schema_definitions' do autoload :IndexDefinition autoload :ColumnDefinition + autoload :ChangeColumnDefinition autoload :TableDefinition autoload :Table autoload :AlterTable @@ -40,6 +42,7 @@ module ActiveRecord autoload :ClosedTransaction autoload :RealTransaction autoload :SavepointTransaction + autoload :TransactionState end # Active Record supports multiple database systems. AbstractAdapter and @@ -105,84 +108,6 @@ module ActiveRecord true end - class SchemaCreation - def initialize(conn) - @conn = conn - @cache = {} - end - - def accept(o) - m = @cache[o.class] ||= "visit_#{o.class.name.split('::').last}" - send m, o - end - - def visit_AddColumn(o) - sql_type = type_to_sql(o.type.to_sym, o.limit, o.precision, o.scale) - sql = "ADD #{quote_column_name(o.name)} #{sql_type}" - add_column_options!(sql, column_options(o)) - end - - private - - def visit_AlterTable(o) - sql = "ALTER TABLE #{quote_table_name(o.name)} " - sql << o.adds.map { |col| visit_AddColumn col }.join(' ') - end - - def visit_ColumnDefinition(o) - sql_type = type_to_sql(o.type.to_sym, o.limit, o.precision, o.scale) - column_sql = "#{quote_column_name(o.name)} #{sql_type}" - add_column_options!(column_sql, column_options(o)) unless o.primary_key? - column_sql - end - - def visit_TableDefinition(o) - create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE " - create_sql << "#{quote_table_name(o.name)} (" - create_sql << o.columns.map { |c| accept c }.join(', ') - create_sql << ") #{o.options}" - create_sql - end - - def column_options(o) - column_options = {} - column_options[:null] = o.null unless o.null.nil? - column_options[:default] = o.default unless o.default.nil? - column_options[:column] = o - column_options[:first] = o.first - column_options[:after] = o.after - column_options - end - - def quote_column_name(name) - @conn.quote_column_name name - end - - def quote_table_name(name) - @conn.quote_table_name name - end - - def type_to_sql(type, limit, precision, scale) - @conn.type_to_sql type.to_sym, limit, precision, scale - end - - def add_column_options!(sql, options) - sql << " DEFAULT #{@conn.quote(options[:default], options[:column])}" if options_include_default?(options) - # must explicitly check for :null to allow change_column to work on migrations - if options[:null] == false - sql << " NOT NULL" - end - if options[:auto_increment] == true - sql << " AUTO_INCREMENT" - end - sql - end - - def options_include_default?(options) - options.include?(:default) && !(options[:null] == false && options[:default].nil?) - end - end - def schema_creation SchemaCreation.new self 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 138ab811dc..dcbc3466b2 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -206,6 +206,12 @@ module ActiveRecord true end + def type_cast(value, column) + return super unless value == true || value == false + + value ? 1 : 0 + end + # MySQL 4 technically support transaction isolation, but it is affected by a bug # where the transaction level gets persisted for the whole session: # @@ -314,27 +320,19 @@ module ActiveRecord def begin_db_transaction execute "BEGIN" - rescue - # Transactions aren't supported end def begin_isolated_db_transaction(isolation) execute "SET TRANSACTION ISOLATION LEVEL #{transaction_isolation_levels.fetch(isolation)}" begin_db_transaction - rescue - # Transactions aren't supported end def commit_db_transaction #:nodoc: execute "COMMIT" - rescue - # Transactions aren't supported end def rollback_db_transaction #:nodoc: execute "ROLLBACK" - rescue - # Transactions aren't supported end # In the simple case, MySQL allows us to place JOINs directly into the UPDATE diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index 88c9494fc6..c4dcba0501 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -160,12 +160,6 @@ module ActiveRecord # QUOTING ================================================== - def type_cast(value, column) - return super unless value == true || value == false - - value ? 1 : 0 - end - def quote_string(string) #:nodoc: @connection.quote(string) end @@ -468,15 +462,17 @@ module ActiveRecord def begin_db_transaction #:nodoc: exec_query "BEGIN" - rescue Mysql::Error - # Transactions aren't supported end private def exec_stmt(sql, name, binds) cache = {} - log(sql, name, binds) do + type_casted_binds = binds.map { |col, val| + [col, type_cast(val, col)] + } + + log(sql, name, type_casted_binds) do if binds.empty? stmt = @connection.prepare(sql) else @@ -487,7 +483,7 @@ module ActiveRecord end begin - stmt.execute(*binds.map { |col, val| type_cast(val, col) }) + stmt.execute(*type_casted_binds.map { |_, val| val }) rescue Mysql::Error => e # Older versions of MySQL leave the prepared statement in a bad # place when an error occurs. To support older mysql versions, we diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb index ea44e818e5..bf34f2bdae 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb @@ -67,7 +67,7 @@ module ActiveRecord end end - def array_to_string(value, column, adapter, should_be_quoted = false) + def array_to_string(value, column, adapter) casted_values = value.map do |val| if String === val if val == "NULL" diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb index e9daa5d7ff..c1f978a081 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -86,8 +86,11 @@ module ActiveRecord case value when Range - return super(value, column) unless /range$/ =~ column.sql_type - PostgreSQLColumn.range_to_string(value) + if /range$/ =~ column.sql_type + PostgreSQLColumn.range_to_string(value) + else + super(value, column) + end when NilClass if column.array && array_member 'NULL' @@ -101,12 +104,21 @@ module ActiveRecord when 'point' then PostgreSQLColumn.point_to_string(value) when 'json' then PostgreSQLColumn.json_to_string(value) else - return super(value, column) unless column.array - PostgreSQLColumn.array_to_string(value, column, self) + if column.array + PostgreSQLColumn.array_to_string(value, column, self) + else + super(value, column) + end end when String - return super(value, column) unless 'bytea' == column.sql_type - { :value => value, :format => 1 } + if 'bytea' == column.sql_type + # Return a bind param hash with format as binary. + # See http://deveiate.org/code/pg/PGconn.html#method-i-exec_prepared-doc + # for more information + { value: value, format: 1 } + else + super(value, column) + end when Hash case column.sql_type when 'hstore' then PostgreSQLColumn.hstore_to_string(value) @@ -114,8 +126,11 @@ module ActiveRecord else super(value, column) end when IPAddr - return super(value, column) unless ['inet','cidr'].include? column.sql_type - PostgreSQLColumn.cidr_to_string(value) + if %w(inet cidr).include? column.sql_type + PostgreSQLColumn.cidr_to_string(value) + else + super(value, column) + end else super(value, column) end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 3668aecd4b..adeb57d913 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -631,7 +631,6 @@ module ActiveRecord true end - # Returns true. def supports_explain? true end @@ -783,11 +782,12 @@ module ActiveRecord def exec_cache(sql, name, binds) stmt_key = prepare_statement(sql) + type_casted_binds = binds.map { |col, val| + [col, type_cast(val, col)] + } - log(sql, name, binds, stmt_key) do - @connection.send_query_prepared(stmt_key, binds.map { |col, val| - type_cast(val, col) - }) + log(sql, name, type_casted_binds, stmt_key) do + @connection.send_query_prepared(stmt_key, type_casted_binds.map { |_, val| val }) @connection.block @connection.get_last_result end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index e5ad08b6b0..2cf1015f2c 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -141,14 +141,12 @@ module ActiveRecord 'SQLite' end - # Returns true def supports_ddl_transactions? true end - # Returns true if SQLite version is '3.6.8' or greater, false otherwise. def supports_savepoints? - sqlite_version >= '3.6.8' + true end # Returns true, since this connection adapter supports prepared statement @@ -162,7 +160,6 @@ module ActiveRecord true end - # Returns true. def supports_primary_key? #:nodoc: true end @@ -171,7 +168,6 @@ module ActiveRecord true end - # Returns true def supports_add_column? true end @@ -193,11 +189,6 @@ module ActiveRecord @statements.clear end - # Returns true - def supports_count_distinct? #:nodoc: - true - end - def supports_index_sort_order? true end @@ -218,7 +209,6 @@ module ActiveRecord @connection.encoding.to_s end - # Returns true. def supports_explain? true end @@ -291,8 +281,11 @@ module ActiveRecord end def exec_query(sql, name = nil, binds = []) - log(sql, name, binds) do + type_casted_binds = binds.map { |col, val| + [col, type_cast(val, col)] + } + log(sql, name, type_casted_binds) do # Don't cache statements if they are not prepared if without_prepared_statement?(binds) stmt = @connection.prepare(sql) @@ -307,9 +300,7 @@ module ActiveRecord stmt = cache[:stmt] cols = cache[:cols] ||= stmt.columns stmt.reset! - stmt.bind_params binds.map { |col, val| - type_cast(val, col) - } + stmt.bind_params type_casted_binds.map { |_, val| val } end ActiveRecord::Result.new(cols, stmt.to_a) diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index d93c26c130..96b5686ae0 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -109,7 +109,7 @@ module ActiveRecord elsif abstract_class? "#{super}(abstract)" elsif !connected? - "#{super}(no database connection)" + "#{super} (call '#{super}.connection' to establish a connection)" elsif table_exists? attr_list = columns.map { |c| "#{c.name}: #{c.type}" } * ', ' "#{super}(#{attr_list})" diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb index e1faadf1ab..3aa5faed87 100644 --- a/activerecord/lib/active_record/counter_cache.rb +++ b/activerecord/lib/active_record/counter_cache.rb @@ -35,7 +35,7 @@ module ActiveRecord stmt = unscoped.where(arel_table[primary_key].eq(object.id)).arel.compile_update({ arel_table[counter_name] => object.send(association).count - }) + }, primary_key) connection.update stmt end return true diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb index f88c8e30e6..27576b1e61 100644 --- a/activerecord/lib/active_record/integration.rb +++ b/activerecord/lib/active_record/integration.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/string/filters' + module ActiveRecord module Integration extend ActiveSupport::Concern @@ -65,5 +67,45 @@ module ActiveRecord "#{self.class.model_name.cache_key}/#{id}" end end + + module ClassMethods + # Defines your model's +to_param+ method to generate "pretty" URLs + # using +method_name+, which can be any attribute or method that + # responds to +to_s+. + # + # class User < ActiveRecord::Base + # to_param :name + # end + # + # user = User.find_by(name: 'Fancy Pants') + # user.id # => 123 + # user_path(user) # => "/users/123-fancy-pants" + # + # Values longer than 20 characters will be truncated. The value + # is truncated word by word. + # + # user = User.find_by(name: 'David HeinemeierHansson') + # user.id # => 125 + # user_path(user) # => "/users/125-david" + # + # Because the generated param begins with the record's +id+, it is + # suitable for passing to +find+. In a controller, for example: + # + # params[:id] # => "123-fancy-pants" + # User.find(params[:id]).id # => 123 + def to_param(method_name = nil) + if method_name.nil? + super() + else + define_method :to_param do + if (default = super()) && (result = send(method_name).to_s).present? + "#{default}-#{result.squish.truncate(20, separator: /\s/, omission: nil).parameterize}" + else + default + end + end + end + end + end end end diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index 55776a91c0..6f54729b3c 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -84,7 +84,10 @@ module ActiveRecord relation.table[self.class.primary_key].eq(id).and( relation.table[lock_col].eq(self.class.quote_value(previous_lock_value, column_for_attribute(lock_col))) ) - ).arel.compile_update(arel_attributes_with_values_for_update(attribute_names)) + ).arel.compile_update( + arel_attributes_with_values_for_update(attribute_names), + self.class.primary_key + ) affected_rows = self.class.connection.update stmt diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb index 927fbab8f0..654ef21b07 100644 --- a/activerecord/lib/active_record/log_subscriber.rb +++ b/activerecord/lib/active_record/log_subscriber.rb @@ -23,6 +23,8 @@ module ActiveRecord def render_bind(column, value) if column if column.binary? + # This specifically deals with the PG adapter that casts bytea columns into a Hash. + value = value[:value] if value.is_a?(Hash) value = "<#{value.bytesize} bytes of binary data>" end diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 5224a6b67c..a4247fb6f4 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -32,7 +32,11 @@ module ActiveRecord class PendingMigrationError < ActiveRecordError#:nodoc: def initialize - super("Migrations are pending; run 'bin/rake db:migrate RAILS_ENV=#{::Rails.env}' to resolve this issue.") + if defined?(Rails) + super("Migrations are pending; run 'bin/rake db:migrate RAILS_ENV=#{::Rails.env}' to resolve this issue.") + else + super("Migrations are pending; run 'bin/rake db:migrate' to resolve this issue.") + end end end @@ -892,7 +896,7 @@ module ActiveRecord validate(@migrations) - ActiveRecord::SchemaMigration.create_table + Base.connection.initialize_schema_migrations_table end def current_version diff --git a/activerecord/lib/active_record/no_touching.rb b/activerecord/lib/active_record/no_touching.rb new file mode 100644 index 0000000000..dbf4564ae5 --- /dev/null +++ b/activerecord/lib/active_record/no_touching.rb @@ -0,0 +1,52 @@ +module ActiveRecord + # = Active Record No Touching + module NoTouching + extend ActiveSupport::Concern + + module ClassMethods + # Lets you selectively disable calls to `touch` for the + # duration of a block. + # + # ==== Examples + # ActiveRecord::Base.no_touching do + # Project.first.touch # does nothing + # Message.first.touch # does nothing + # end + # + # Project.no_touching do + # Project.first.touch # does nothing + # Message.first.touch # works, but does not touch the associated project + # end + # + def no_touching(&block) + NoTouching.apply_to(self, &block) + end + end + + class << self + def apply_to(klass) #:nodoc: + klasses.push(klass) + yield + ensure + klasses.pop + end + + def applied_to?(klass) #:nodoc: + klasses.any? { |k| k >= klass } + end + + private + def klasses + Thread.current[:no_touching_classes] ||= [] + end + end + + def no_touching? + NoTouching.applied_to?(self.class) + end + + def touch(*) + super unless no_touching? + end + end +end diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 60f2726a6e..745c6cf349 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -7,7 +7,7 @@ module ActiveRecord MULTI_VALUE_METHODS = [:includes, :eager_load, :preload, :select, :group, :order, :joins, :where, :having, :bind, :references, - :extending] + :extending, :unscope] SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :from, :reordering, :reverse_order, :distinct, :create_with, :uniq] @@ -71,7 +71,7 @@ module ActiveRecord def update_record(values, id, id_was) # :nodoc: substitutes, binds = substitute_values values - um = @klass.unscoped.where(@klass.arel_table[@klass.primary_key].eq(id_was || id)).arel.compile_update(substitutes) + um = @klass.unscoped.where(@klass.arel_table[@klass.primary_key].eq(id_was || id)).arel.compile_update(substitutes, @klass.primary_key) @klass.connection.update( um, @@ -244,7 +244,13 @@ module ActiveRecord def empty? return @records.empty? if loaded? - limit_value == 0 ? true : !exists? + if limit_value == 0 + true + else + # FIXME: This count is not compatible with #select('authors.*') or other select narrows + c = count + c.respond_to?(:zero?) ? c.zero? : c.empty? + end end # Returns true if there are any records. diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 3a02bf90e9..d91d6367a3 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -6,11 +6,12 @@ module ActiveRecord # If no record can be found for all of the listed ids, then RecordNotFound will be raised. If the primary key # is an integer, find by id coerces its arguments using +to_i+. # - # Person.find(1) # returns the object for ID = 1 - # Person.find("1") # returns the object for ID = 1 - # Person.find(1, 2, 6) # returns an array for objects with IDs in (1, 2, 6) - # Person.find([7, 17]) # returns an array for objects with IDs in (7, 17) - # Person.find([1]) # returns an array for the object with ID = 1 + # Person.find(1) # returns the object for ID = 1 + # Person.find("1") # returns the object for ID = 1 + # Person.find("31-sarah") # returns the object for ID = 31 + # Person.find(1, 2, 6) # returns an array for objects with IDs in (1, 2, 6) + # Person.find([7, 17]) # returns an array for objects with IDs in (7, 17) + # Person.find([1]) # returns an array for the object with ID = 1 # Person.where("administrator = 1").order("created_on DESC").find(1) # # <tt>ActiveRecord::RecordNotFound</tt> will be raised if one or more ids are not found. diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index a9dbc14d6e..cacc787eba 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -341,13 +341,19 @@ module ActiveRecord # User.where(name: "John", active: true).unscope(where: :name) # == User.where(active: true) # - # Note that this method is more generalized than ActiveRecord::SpawnMethods#except - # because #except will only affect a particular relation's values. It won't wipe - # the order, grouping, etc. when that relation is merged. For example: + # This method is similar to <tt>except</tt>, but unlike + # <tt>except</tt>, it persists across merges: # - # Post.comments.except(:order) + # User.order('email').merge(User.except(:order)) + # == User.order('email') + # + # User.order('email').merge(User.unscope(:order)) + # == User.all + # + # This means it can be used in association definitions: + # + # has_many :comments, -> { unscope where: :trashed } # - # will still have an order if it comes from the default_scope on Comment. def unscope(*args) check_if_method_has_arguments!(:unscope, args) spawn.unscope!(*args) @@ -355,6 +361,7 @@ module ActiveRecord def unscope!(*args) # :nodoc: args.flatten! + self.unscope_values += args args.each do |scope| case scope @@ -995,12 +1002,6 @@ module ActiveRecord s.strip! s.gsub!(/\sasc\Z/i, ' DESC') || s.gsub!(/\sdesc\Z/i, ' ASC') || s.concat(' DESC') end - when Symbol - { o => :desc } - when Hash - o.each_with_object({}) do |(field, dir), memo| - memo[field] = (dir == :asc ? :desc : :asc ) - end else o end @@ -1016,17 +1017,6 @@ module ActiveRecord orders.reject!(&:blank?) orders = reverse_sql_order(orders) if reverse_order_value - orders = orders.flat_map do |order| - case order - when Symbol - table[order].asc - when Hash - order.map { |field, dir| table[field].send(dir) } - else - order - end - end - arel.order(*orders) unless orders.empty? end @@ -1048,8 +1038,17 @@ module ActiveRecord # if a symbol is given we prepend the quoted table name order_args.map! do |arg| - arg.is_a?(Symbol) ? Arel::Nodes::Ascending.new(klass.arel_table[arg]) : arg - end + case arg + when Symbol + table[arg].asc + when Hash + arg.map { |field, dir| + table[field].send(dir) + } + else + arg + end + end.flatten! end # Checks to make sure that the arguments are not blank. Note that if some diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb index 1dc3bf3f12..469451e2f4 100644 --- a/activerecord/lib/active_record/result.rb +++ b/activerecord/lib/active_record/result.rb @@ -83,9 +83,10 @@ module ActiveRecord end def initialize_copy(other) - @columns = columns.dup - @rows = rows.dup - @hash_rows = nil + @columns = columns.dup + @rows = rows.dup + @column_types = column_types.dup + @hash_rows = nil end private diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb index 301bc503c7..186a737561 100644 --- a/activerecord/lib/active_record/store.rb +++ b/activerecord/lib/active_record/store.rb @@ -15,9 +15,10 @@ module ActiveRecord # You can set custom coder to encode/decode your serialized attributes to/from different formats. # JSON, YAML, Marshal are supported out of the box. Generally it can be any wrapper that provides +load+ and +dump+. # - # NOTE - If you are using special PostgreSQL columns like +hstore+ or +json+ there is no need for - # the serialization provieded by +store+. You can simply use +store_accessor+ instead to generate - # the accessor methods. + # NOTE - If you are using PostgreSQL specific columns like +hstore+ or +json+ there is no need for + # the serialization provided by +store+. Simply use +store_accessor+ instead to generate + # the accessor methods. Be aware that these columns use a string keyed hash and do not allow access + # using a symbol. # # Examples: # diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index b55af692d6..38f37f5c8a 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -48,7 +48,7 @@ module ActiveRecord def build_relation(klass, table, attribute, value) #:nodoc: if reflection = klass.reflect_on_association(attribute) attribute = reflection.foreign_key - value = value.attributes[reflection.primary_key_column.name] + value = value.attributes[reflection.primary_key_column.name] unless value.nil? end column = klass.columns_hash[attribute.to_s] |