aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord/lib/active_record')
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_association.rb12
-rw-r--r--activerecord/lib/active_record/associations/has_one_association.rb14
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb639
-rw-r--r--activerecord/lib/active_record/fixtures.rb4
-rw-r--r--activerecord/lib/active_record/persistence.rb63
-rw-r--r--activerecord/lib/active_record/relation/calculations.rb1
-rw-r--r--activerecord/lib/active_record/relation/finder_methods.rb4
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb14
-rw-r--r--activerecord/lib/active_record/relation/spawn_methods.rb8
-rw-r--r--activerecord/lib/active_record/timestamp.rb39
10 files changed, 724 insertions, 74 deletions
diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb
index 4558872a2b..2eb56e5cd3 100644
--- a/activerecord/lib/active_record/associations/belongs_to_association.rb
+++ b/activerecord/lib/active_record/associations/belongs_to_association.rb
@@ -49,12 +49,16 @@ module ActiveRecord
else
"find"
end
+
+ options = @reflection.options.dup
+ (options.keys - [:select, :include, :readonly]).each do |key|
+ options.delete key
+ end
+ options[:conditions] = conditions
+
the_target = @reflection.klass.send(find_method,
@owner[@reflection.primary_key_name],
- :select => @reflection.options[:select],
- :conditions => conditions,
- :include => @reflection.options[:include],
- :readonly => @reflection.options[:readonly]
+ options
) if @owner[@reflection.primary_key_name]
set_inverse_instance(the_target, @owner)
the_target
diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb
index 68b8b792ad..a6e6bfa356 100644
--- a/activerecord/lib/active_record/associations/has_one_association.rb
+++ b/activerecord/lib/active_record/associations/has_one_association.rb
@@ -79,13 +79,13 @@ module ActiveRecord
private
def find_target
- the_target = @reflection.klass.find(:first,
- :conditions => @finder_sql,
- :select => @reflection.options[:select],
- :order => @reflection.options[:order],
- :include => @reflection.options[:include],
- :readonly => @reflection.options[:readonly]
- )
+ options = @reflection.options.dup
+ (options.keys - [:select, :order, :include, :readonly]).each do |key|
+ options.delete key
+ end
+ options[:conditions] = @finder_sql
+
+ the_target = @reflection.klass.find(:first, options)
set_inverse_instance(the_target, @owner)
the_target
end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
new file mode 100644
index 0000000000..568759775b
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
@@ -0,0 +1,639 @@
+# encoding: utf-8
+
+require 'mysql2' unless defined? Mysql2
+
+module ActiveRecord
+ class Base
+ def self.mysql2_connection(config)
+ config[:username] = 'root' if config[:username].nil?
+ client = Mysql2::Client.new(config.symbolize_keys)
+ options = [config[:host], config[:username], config[:password], config[:database], config[:port], config[:socket], 0]
+ ConnectionAdapters::Mysql2Adapter.new(client, logger, options, config)
+ end
+ end
+
+ module ConnectionAdapters
+ class Mysql2Column < Column
+ BOOL = "tinyint(1)"
+ def extract_default(default)
+ if sql_type =~ /blob/i || type == :text
+ if default.blank?
+ return null ? nil : ''
+ else
+ raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}"
+ end
+ elsif missing_default_forged_as_empty_string?(default)
+ nil
+ else
+ super
+ end
+ end
+
+ def has_default?
+ return false if sql_type =~ /blob/i || type == :text #mysql forbids defaults on blob and text columns
+ super
+ end
+
+ # Returns the Ruby class that corresponds to the abstract data type.
+ def klass
+ case type
+ when :integer then Fixnum
+ when :float then Float
+ when :decimal then BigDecimal
+ when :datetime then Time
+ when :date then Date
+ when :timestamp then Time
+ when :time then Time
+ when :text, :string then String
+ when :binary then String
+ when :boolean then Object
+ end
+ end
+
+ def type_cast(value)
+ return nil if value.nil?
+ case type
+ when :string then value
+ when :text then value
+ when :integer then value.to_i rescue value ? 1 : 0
+ when :float then value.to_f # returns self if it's already a Float
+ when :decimal then self.class.value_to_decimal(value)
+ when :datetime, :timestamp then value.class == Time ? value : self.class.string_to_time(value)
+ when :time then value.class == Time ? value : self.class.string_to_dummy_time(value)
+ when :date then value.class == Date ? value : self.class.string_to_date(value)
+ when :binary then value
+ when :boolean then self.class.value_to_boolean(value)
+ else value
+ end
+ end
+
+ def type_cast_code(var_name)
+ case type
+ when :string then nil
+ when :text then nil
+ when :integer then "#{var_name}.to_i rescue #{var_name} ? 1 : 0"
+ when :float then "#{var_name}.to_f"
+ when :decimal then "#{self.class.name}.value_to_decimal(#{var_name})"
+ when :datetime, :timestamp then "#{var_name}.class == Time ? #{var_name} : #{self.class.name}.string_to_time(#{var_name})"
+ when :time then "#{var_name}.class == Time ? #{var_name} : #{self.class.name}.string_to_dummy_time(#{var_name})"
+ when :date then "#{var_name}.class == Date ? #{var_name} : #{self.class.name}.string_to_date(#{var_name})"
+ when :binary then nil
+ when :boolean then "#{self.class.name}.value_to_boolean(#{var_name})"
+ else nil
+ end
+ end
+
+ private
+ def simplified_type(field_type)
+ return :boolean if Mysql2Adapter.emulate_booleans && field_type.downcase.index(BOOL)
+ return :string if field_type =~ /enum/i or field_type =~ /set/i
+ return :integer if field_type =~ /year/i
+ return :binary if field_type =~ /bit/i
+ super
+ end
+
+ def extract_limit(sql_type)
+ case sql_type
+ when /blob|text/i
+ case sql_type
+ when /tiny/i
+ 255
+ when /medium/i
+ 16777215
+ when /long/i
+ 2147483647 # mysql only allows 2^31-1, not 2^32-1, somewhat inconsistently with the tiny/medium/normal cases
+ else
+ super # we could return 65535 here, but we leave it undecorated by default
+ end
+ when /^bigint/i; 8
+ when /^int/i; 4
+ when /^mediumint/i; 3
+ when /^smallint/i; 2
+ when /^tinyint/i; 1
+ else
+ super
+ end
+ end
+
+ # MySQL misreports NOT NULL column default when none is given.
+ # We can't detect this for columns which may have a legitimate ''
+ # default (string) but we can for others (integer, datetime, boolean,
+ # and the rest).
+ #
+ # Test whether the column has default '', is not null, and is not
+ # a type allowing default ''.
+ def missing_default_forged_as_empty_string?(default)
+ type != :string && !null && default == ''
+ end
+ end
+
+ class Mysql2Adapter < AbstractAdapter
+ cattr_accessor :emulate_booleans
+ self.emulate_booleans = true
+
+ ADAPTER_NAME = 'Mysql2'
+ PRIMARY = "PRIMARY"
+
+ LOST_CONNECTION_ERROR_MESSAGES = [
+ "Server shutdown in progress",
+ "Broken pipe",
+ "Lost connection to MySQL server during query",
+ "MySQL server has gone away" ]
+
+ QUOTED_TRUE, QUOTED_FALSE = '1', '0'
+
+ NATIVE_DATABASE_TYPES = {
+ :primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY",
+ :string => { :name => "varchar", :limit => 255 },
+ :text => { :name => "text" },
+ :integer => { :name => "int", :limit => 4 },
+ :float => { :name => "float" },
+ :decimal => { :name => "decimal" },
+ :datetime => { :name => "datetime" },
+ :timestamp => { :name => "datetime" },
+ :time => { :name => "time" },
+ :date => { :name => "date" },
+ :binary => { :name => "blob" },
+ :boolean => { :name => "tinyint", :limit => 1 }
+ }
+
+ def initialize(connection, logger, connection_options, config)
+ super(connection, logger)
+ @connection_options, @config = connection_options, config
+ @quoted_column_names, @quoted_table_names = {}, {}
+ configure_connection
+ end
+
+ def adapter_name
+ ADAPTER_NAME
+ end
+
+ def supports_migrations?
+ true
+ end
+
+ def supports_primary_key?
+ true
+ end
+
+ def supports_savepoints?
+ true
+ end
+
+ def native_database_types
+ NATIVE_DATABASE_TYPES
+ end
+
+ # QUOTING ==================================================
+
+ def quote(value, column = nil)
+ if value.kind_of?(String) && column && column.type == :binary && column.class.respond_to?(:string_to_binary)
+ s = column.class.string_to_binary(value).unpack("H*")[0]
+ "x'#{s}'"
+ elsif value.kind_of?(BigDecimal)
+ value.to_s("F")
+ else
+ super
+ end
+ end
+
+ def quote_column_name(name) #:nodoc:
+ @quoted_column_names[name] ||= "`#{name}`"
+ end
+
+ def quote_table_name(name) #:nodoc:
+ @quoted_table_names[name] ||= quote_column_name(name).gsub('.', '`.`')
+ end
+
+ def quote_string(string)
+ @connection.escape(string)
+ end
+
+ def quoted_true
+ QUOTED_TRUE
+ end
+
+ def quoted_false
+ QUOTED_FALSE
+ end
+
+ # REFERENTIAL INTEGRITY ====================================
+
+ def disable_referential_integrity(&block) #:nodoc:
+ old = select_value("SELECT @@FOREIGN_KEY_CHECKS")
+
+ begin
+ update("SET FOREIGN_KEY_CHECKS = 0")
+ yield
+ ensure
+ update("SET FOREIGN_KEY_CHECKS = #{old}")
+ end
+ end
+
+ # CONNECTION MANAGEMENT ====================================
+
+ def active?
+ return false unless @connection
+ @connection.query 'select 1'
+ true
+ rescue Mysql2::Error
+ false
+ end
+
+ def reconnect!
+ disconnect!
+ connect
+ end
+
+ # this is set to true in 2.3, but we don't want it to be
+ def requires_reloading?
+ false
+ end
+
+ def disconnect!
+ unless @connection.nil?
+ @connection.close
+ @connection = nil
+ end
+ end
+
+ def reset!
+ disconnect!
+ connect
+ end
+
+ # DATABASE STATEMENTS ======================================
+
+ # FIXME: re-enable the following once a "better" query_cache solution is in core
+ #
+ # The overrides below perform much better than the originals in AbstractAdapter
+ # because we're able to take advantage of mysql2's lazy-loading capabilities
+ #
+ # # Returns a record hash with the column names as keys and column values
+ # # as values.
+ # def select_one(sql, name = nil)
+ # result = execute(sql, name)
+ # result.each(:as => :hash) do |r|
+ # return r
+ # end
+ # end
+ #
+ # # Returns a single value from a record
+ # def select_value(sql, name = nil)
+ # result = execute(sql, name)
+ # if first = result.first
+ # first.first
+ # end
+ # end
+ #
+ # # Returns an array of the values of the first column in a select:
+ # # select_values("SELECT id FROM companies LIMIT 3") => [1,2,3]
+ # def select_values(sql, name = nil)
+ # execute(sql, name).map { |row| row.first }
+ # 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)
+ execute(sql, name).to_a
+ end
+
+ # Executes the SQL statement in the context of this connection.
+ def execute(sql, name = nil)
+ # make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been
+ # made since we established the connection
+ @connection.query_options[:database_timezone] = ActiveRecord::Base.default_timezone
+ if name == :skip_logging
+ @connection.query(sql)
+ else
+ log(sql, name) { @connection.query(sql) }
+ end
+ rescue ActiveRecord::StatementInvalid => exception
+ if exception.message.split(":").first =~ /Packets out of order/
+ raise ActiveRecord::StatementInvalid, "'Packets out of order' error was received from the database. Please update your mysql bindings (gem install mysql) and read http://dev.mysql.com/doc/mysql/en/password-hashing.html for more information. If you're on Windows, use the Instant Rails installer to get the updated mysql bindings."
+ else
+ raise
+ end
+ end
+
+ def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
+ super
+ id_value || @connection.last_id
+ end
+ alias :create :insert_sql
+
+ def update_sql(sql, name = nil)
+ super
+ @connection.affected_rows
+ end
+
+ def begin_db_transaction
+ execute "BEGIN"
+ rescue Exception
+ # Transactions aren't supported
+ end
+
+ def commit_db_transaction
+ execute "COMMIT"
+ rescue Exception
+ # Transactions aren't supported
+ end
+
+ def rollback_db_transaction
+ execute "ROLLBACK"
+ rescue Exception
+ # Transactions aren't supported
+ end
+
+ def create_savepoint
+ execute("SAVEPOINT #{current_savepoint_name}")
+ end
+
+ def rollback_to_savepoint
+ execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}")
+ end
+
+ def release_savepoint
+ execute("RELEASE SAVEPOINT #{current_savepoint_name}")
+ end
+
+ def add_limit_offset!(sql, options)
+ limit, offset = options[:limit], options[:offset]
+ if limit && offset
+ sql << " LIMIT #{offset.to_i}, #{sanitize_limit(limit)}"
+ elsif limit
+ sql << " LIMIT #{sanitize_limit(limit)}"
+ elsif offset
+ sql << " OFFSET #{offset.to_i}"
+ end
+ sql
+ end
+
+ # SCHEMA STATEMENTS ========================================
+
+ def structure_dump
+ if supports_views?
+ sql = "SHOW FULL TABLES WHERE Table_type = 'BASE TABLE'"
+ else
+ sql = "SHOW TABLES"
+ end
+
+ select_all(sql).inject("") do |structure, table|
+ table.delete('Table_type')
+ structure += select_one("SHOW CREATE TABLE #{quote_table_name(table.to_a.first.last)}")["Create Table"] + ";\n\n"
+ end
+ end
+
+ def recreate_database(name, options = {})
+ drop_database(name)
+ create_database(name, options)
+ end
+
+ # Create a new MySQL database with optional <tt>:charset</tt> and <tt>:collation</tt>.
+ # Charset defaults to utf8.
+ #
+ # Example:
+ # create_database 'charset_test', :charset => 'latin1', :collation => 'latin1_bin'
+ # create_database 'matt_development'
+ # create_database 'matt_development', :charset => :big5
+ def create_database(name, options = {})
+ if options[:collation]
+ execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}` COLLATE `#{options[:collation]}`"
+ else
+ execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}`"
+ end
+ end
+
+ def drop_database(name) #:nodoc:
+ execute "DROP DATABASE IF EXISTS `#{name}`"
+ end
+
+ def current_database
+ select_value 'SELECT DATABASE() as db'
+ end
+
+ # Returns the database character set.
+ def charset
+ show_variable 'character_set_database'
+ end
+
+ # Returns the database collation strategy.
+ def collation
+ show_variable 'collation_database'
+ end
+
+ def tables(name = nil)
+ tables = []
+ execute("SHOW TABLES", name).each do |field|
+ tables << field.first
+ end
+ tables
+ end
+
+ def drop_table(table_name, options = {})
+ super(table_name, options)
+ end
+
+ def indexes(table_name, name = nil)
+ indexes = []
+ current_index = nil
+ result = execute("SHOW KEYS FROM #{quote_table_name(table_name)}", name)
+ result.each(:symbolize_keys => true, :as => :hash) do |row|
+ if current_index != row[:Key_name]
+ next if row[:Key_name] == PRIMARY # skip the primary key
+ current_index = row[:Key_name]
+ indexes << IndexDefinition.new(row[:Table], row[:Key_name], row[:Non_unique] == 0, [])
+ end
+
+ indexes.last.columns << row[:Column_name]
+ end
+ indexes
+ end
+
+ def columns(table_name, name = nil)
+ sql = "SHOW FIELDS FROM #{quote_table_name(table_name)}"
+ columns = []
+ result = execute(sql, :skip_logging)
+ result.each(:symbolize_keys => true, :as => :hash) { |field|
+ columns << Mysql2Column.new(field[:Field], field[:Default], field[:Type], field[:Null] == "YES")
+ }
+ columns
+ end
+
+ def create_table(table_name, options = {})
+ super(table_name, options.reverse_merge(:options => "ENGINE=InnoDB"))
+ end
+
+ def rename_table(table_name, new_name)
+ execute "RENAME TABLE #{quote_table_name(table_name)} TO #{quote_table_name(new_name)}"
+ end
+
+ def add_column(table_name, column_name, type, options = {})
+ add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
+ add_column_options!(add_column_sql, options)
+ add_column_position!(add_column_sql, options)
+ execute(add_column_sql)
+ end
+
+ def change_column_default(table_name, column_name, default)
+ column = column_for(table_name, column_name)
+ change_column table_name, column_name, column.sql_type, :default => default
+ end
+
+ def change_column_null(table_name, column_name, null, default = nil)
+ column = column_for(table_name, column_name)
+
+ unless null || default.nil?
+ execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
+ end
+
+ change_column table_name, column_name, column.sql_type, :null => null
+ end
+
+ def change_column(table_name, column_name, type, options = {})
+ column = column_for(table_name, column_name)
+
+ unless options_include_default?(options)
+ options[:default] = column.default
+ end
+
+ unless options.has_key?(:null)
+ options[:null] = column.null
+ end
+
+ change_column_sql = "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
+ add_column_options!(change_column_sql, options)
+ add_column_position!(change_column_sql, options)
+ execute(change_column_sql)
+ end
+
+ def rename_column(table_name, column_name, new_column_name)
+ options = {}
+ if column = columns(table_name).find { |c| c.name == column_name.to_s }
+ options[:default] = column.default
+ options[:null] = column.null
+ else
+ raise ActiveRecordError, "No such column: #{table_name}.#{column_name}"
+ end
+ current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'")["Type"]
+ rename_column_sql = "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(new_column_name)} #{current_type}"
+ add_column_options!(rename_column_sql, options)
+ execute(rename_column_sql)
+ end
+
+ # Maps logical Rails types to MySQL-specific data types.
+ def type_to_sql(type, limit = nil, precision = nil, scale = nil)
+ return super unless type.to_s == 'integer'
+
+ case limit
+ when 1; 'tinyint'
+ when 2; 'smallint'
+ when 3; 'mediumint'
+ when nil, 4, 11; 'int(11)' # compatibility with MySQL default
+ when 5..8; 'bigint'
+ else raise(ActiveRecordError, "No integer type has byte size #{limit}")
+ end
+ end
+
+ def add_column_position!(sql, options)
+ if options[:first]
+ sql << " FIRST"
+ elsif options[:after]
+ sql << " AFTER #{quote_column_name(options[:after])}"
+ end
+ end
+
+ def show_variable(name)
+ variables = select_all("SHOW VARIABLES LIKE '#{name}'")
+ variables.first['Value'] unless variables.empty?
+ end
+
+ def pk_and_sequence_for(table)
+ keys = []
+ result = execute("describe #{quote_table_name(table)}")
+ result.each(:symbolize_keys => true, :as => :hash) do |row|
+ keys << row[:Field] if row[:Key] == "PRI"
+ end
+ keys.length == 1 ? [keys.first, nil] : nil
+ end
+
+ # Returns just a table's primary key
+ def primary_key(table)
+ pk_and_sequence = pk_and_sequence_for(table)
+ pk_and_sequence && pk_and_sequence.first
+ end
+
+ def case_sensitive_equality_operator
+ "= BINARY"
+ end
+
+ def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key)
+ where_sql
+ end
+
+ protected
+ def quoted_columns_for_index(column_names, options = {})
+ length = options[:length] if options.is_a?(Hash)
+
+ quoted_column_names = case length
+ when Hash
+ column_names.map {|name| length[name] ? "#{quote_column_name(name)}(#{length[name]})" : quote_column_name(name) }
+ when Fixnum
+ column_names.map {|name| "#{quote_column_name(name)}(#{length})"}
+ else
+ column_names.map {|name| quote_column_name(name) }
+ end
+ end
+
+ def translate_exception(exception, message)
+ return super unless exception.respond_to?(:error_number)
+
+ case exception.error_number
+ when 1062
+ RecordNotUnique.new(message, exception)
+ when 1452
+ InvalidForeignKey.new(message, exception)
+ else
+ super
+ end
+ end
+
+ private
+ def connect
+ @connection = Mysql2::Client.new(@config)
+ configure_connection
+ end
+
+ def configure_connection
+ @connection.query_options.merge!(:as => :array)
+ encoding = @config[:encoding]
+ execute("SET NAMES '#{encoding}'", :skip_logging) if encoding
+
+ # By default, MySQL 'where id is null' selects the last inserted id.
+ # Turn this off. http://dev.rubyonrails.org/ticket/6778
+ execute("SET SQL_AUTO_IS_NULL=0", :skip_logging)
+ end
+
+ # Returns an array of record hashes with the column names as keys and
+ # column values as values.
+ def select(sql, name = nil)
+ execute(sql, name).each(:as => :hash)
+ end
+
+ def supports_views?
+ version[0] >= 5
+ end
+
+ def version
+ @version ||= @connection.info[:version].scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i }
+ end
+
+ def column_for(table_name, column_name)
+ unless column = columns(table_name).find { |c| c.name == column_name.to_s }
+ raise "No such column: #{table_name}.#{column_name}"
+ end
+ column
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb
index e44102b538..4e49e9f720 100644
--- a/activerecord/lib/active_record/fixtures.rb
+++ b/activerecord/lib/active_record/fixtures.rb
@@ -24,6 +24,8 @@ else
end
end
+class FixturesFileNotFound < StandardError; end
+
# Fixtures are a way of organizing data that you want to test against; in short, sample data.
#
# = Fixture formats
@@ -696,6 +698,8 @@ class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash)
read_yaml_fixture_files
elsif File.file?(csv_file_path)
read_csv_fixture_files
+ else
+ raise FixturesFileNotFound, "Could not find #{yaml_file_path} or #{csv_file_path}"
end
end
diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb
index 71b46beaef..0188972169 100644
--- a/activerecord/lib/active_record/persistence.rb
+++ b/activerecord/lib/active_record/persistence.rb
@@ -1,7 +1,7 @@
module ActiveRecord
# = Active Record Persistence
module Persistence
- # Returns true if this object hasn't been saved yet -- that is, a record
+ # Returns true if this object hasn't been saved yet -- that is, a record
# for the object doesn't exist in the data store yet; otherwise, returns false.
def new_record?
@new_record
@@ -72,7 +72,7 @@ module ActiveRecord
freeze
end
- # Deletes the record in the database and freezes this instance to reflect
+ # Deletes the record in the database and freezes this instance to reflect
# that no changes should be made (since they can't be persisted).
def destroy
if persisted?
@@ -83,15 +83,15 @@ module ActiveRecord
freeze
end
- # Returns an instance of the specified +klass+ with the attributes of the
- # current record. This is mostly useful in relation to single-table
- # inheritance structures where you want a subclass to appear as the
- # superclass. This can be used along with record identification in
- # Action Pack to allow, say, <tt>Client < Company</tt> to do something
+ # Returns an instance of the specified +klass+ with the attributes of the
+ # current record. This is mostly useful in relation to single-table
+ # inheritance structures where you want a subclass to appear as the
+ # superclass. This can be used along with record identification in
+ # Action Pack to allow, say, <tt>Client < Company</tt> to do something
# like render <tt>:partial => @client.becomes(Company)</tt> to render that
# instance using the companies/company partial instead of clients/client.
#
- # Note: The new instance will share a link to the same attributes as the original class.
+ # Note: The new instance will share a link to the same attributes as the original class.
# So any change to the attributes in either instance will affect the other.
def becomes(klass)
became = klass.new
@@ -102,34 +102,19 @@ module ActiveRecord
became
end
- # Updates a single attribute and saves the record.
+ # Updates a single attribute and saves the record.
# This is especially useful for boolean flags on existing records. Also note that
#
- # * The attribute being updated must be a column name.
# * Validation is skipped.
- # * No callbacks are invoked.
+ # * Callbacks are invoked.
# * updated_at/updated_on column is updated if that column is available.
- # * Does not work on associations.
- # * Does not work on attr_accessor attributes.
- # * Does not work on new record. <tt>record.new_record?</tt> should return false for this method to work.
- # * Updates only the attribute that is input to the method. If there are other changed attributes then
- # those attributes are left alone. In that case even after this method has done its work <tt>record.changed?</tt>
- # will return true.
+ # * Updates all the attributes that are dirty in this object.
#
def update_attribute(name, value)
- raise ActiveRecordError, "#{name.to_s} is marked as readonly" if self.class.readonly_attributes.include? name.to_s
-
- changes = record_update_timestamps || {}
-
- if name
- name = name.to_s
- send("#{name}=", value)
- changes[name] = read_attribute(name)
- end
-
- @changed_attributes.except!(*changes.keys)
- primary_key = self.class.primary_key
- self.class.update_all(changes, { primary_key => self[primary_key] }) == 1
+ name = name.to_s
+ raise ActiveRecordError, "#{name} is marked as readonly" if self.class.readonly_attributes.include?(name)
+ send("#{name}=", value)
+ save(:validate => false)
end
# Updates the attributes of the model from the passed-in hash and saves the
@@ -220,15 +205,27 @@ module ActiveRecord
# Saves the record with the updated_at/on attributes set to the current time.
# Please note that no validation is performed and no callbacks are executed.
- # If an attribute name is passed, that attribute is updated along with
+ # If an attribute name is passed, that attribute is updated along with
# updated_at/on attributes.
#
# Examples:
#
# product.touch # updates updated_at/on
# product.touch(:designed_at) # updates the designed_at attribute and updated_at/on
- def touch(attribute = nil)
- update_attribute(attribute, current_time_from_proper_timezone)
+ def touch(name = nil)
+ attributes = timestamp_attributes_for_update_in_model
+ attributes << name if name
+
+ current_time = current_time_from_proper_timezone
+ changes = {}
+
+ attributes.each do |column|
+ changes[column.to_s] = write_attribute(column.to_s, current_time)
+ end
+
+ @changed_attributes.except!(*changes.keys)
+ primary_key = self.class.primary_key
+ self.class.update_all(changes, { primary_key => self[primary_key] }) == 1
end
private
diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb
index f8412bc604..a679c444cf 100644
--- a/activerecord/lib/active_record/relation/calculations.rb
+++ b/activerecord/lib/active_record/relation/calculations.rb
@@ -1,4 +1,5 @@
require 'active_support/core_ext/object/blank'
+require 'active_support/core_ext/object/try'
module ActiveRecord
module Calculations
diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb
index b34c11973b..0c75acf723 100644
--- a/activerecord/lib/active_record/relation/finder_methods.rb
+++ b/activerecord/lib/active_record/relation/finder_methods.rb
@@ -291,7 +291,7 @@ module ActiveRecord
record = where(primary_key.eq(id)).first
unless record
- conditions = arel.send(:where_clauses).join(', ')
+ conditions = arel.wheres.map { |x| x.value }.join(', ')
conditions = " [WHERE #{conditions}]" if conditions.present?
raise RecordNotFound, "Couldn't find #{@klass.name} with ID=#{id}#{conditions}"
end
@@ -317,7 +317,7 @@ module ActiveRecord
if result.size == expected_size
result
else
- conditions = arel.send(:where_clauses).join(', ')
+ conditions = arel.wheres.map { |x| x.value }.join(', ')
conditions = " [WHERE #{conditions}]" if conditions.present?
error = "Couldn't find all #{@klass.name.pluralize} with IDs "
diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb
index e71f1cca72..cd6c6f8d1f 100644
--- a/activerecord/lib/active_record/relation/query_methods.rb
+++ b/activerecord/lib/active_record/relation/query_methods.rb
@@ -49,7 +49,9 @@ module ActiveRecord
def where(opts, *rest)
value = build_where(opts, rest)
- value ? clone.tap {|r| r.where_values += Array.wrap(value) } : clone
+ copy = clone
+ copy.where_values += Array.wrap(value) if value
+ copy
end
def having(*args)
@@ -58,7 +60,9 @@ module ActiveRecord
end
def limit(value = true)
- clone.tap {|r| r.limit_value = value }
+ copy = clone
+ copy.limit_value = value
+ copy
end
def offset(value = true)
@@ -131,9 +135,7 @@ module ActiveRecord
arel = build_joins(arel, @joins_values) unless @joins_values.empty?
- @where_values.uniq.each do |where|
- next if where.blank?
-
+ (@where_values - ['']).uniq.each do |where|
case where
when Arel::SqlLiteral
arel = arel.where(where)
@@ -217,7 +219,7 @@ module ActiveRecord
end
def build_select(arel, selects)
- if selects.present?
+ unless selects.empty?
@implicit_readonly = false
# TODO: fix this ugly hack, we should refactor the callers to get an ARel compatible array.
# Before this change we were passing to ARel the last element only, and ARel is capable of handling an array
diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb
index 7712ad2569..02db8d2b89 100644
--- a/activerecord/lib/active_record/relation/spawn_methods.rb
+++ b/activerecord/lib/active_record/relation/spawn_methods.rb
@@ -80,10 +80,14 @@ module ActiveRecord
options.assert_valid_keys(VALID_FIND_OPTIONS)
- [:joins, :select, :group, :having, :limit, :offset, :from, :lock, :readonly].each do |finder|
- relation = relation.send(finder, options[finder]) if options.has_key?(finder)
+ [:joins, :select, :group, :having, :limit, :offset, :from, :lock].each do |finder|
+ if value = options[finder]
+ relation = relation.send(finder, value)
+ end
end
+ relation = relation.readonly(options[:readonly]) if options.key? :readonly
+
# Give precedence to newly-applied orders and groups to play nicely with with_scope
[:group, :order].each do |finder|
relation.send("#{finder}_values=", Array.wrap(options[finder]) + relation.send("#{finder}_values")) if options.has_key?(finder)
diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb
index 5531d12a41..c6ff4b39fa 100644
--- a/activerecord/lib/active_record/timestamp.rb
+++ b/activerecord/lib/active_record/timestamp.rb
@@ -1,8 +1,8 @@
module ActiveRecord
# = Active Record Timestamp
- #
+ #
# Active Record automatically timestamps create and update operations if the
- # table has fields named <tt>created_at/created_on</tt> or
+ # table has fields named <tt>created_at/created_on</tt> or
# <tt>updated_at/updated_on</tt>.
#
# Timestamping can be turned off by setting:
@@ -21,7 +21,7 @@ module ActiveRecord
#
# This feature can easily be turned off by assigning value <tt>false</tt> .
#
- # If your attributes are time zone aware and you desire to skip time zone conversion for certain
+ # If your attributes are time zone aware and you desire to skip time zone conversion for certain
# attributes then you can do following:
#
# Topic.skip_time_zone_conversion_for_attributes = [:written_on]
@@ -39,34 +39,33 @@ module ActiveRecord
if record_timestamps
current_time = current_time_from_proper_timezone
- timestamp_attributes_for_create.each do |column|
+ all_timestamp_attributes.each do |column|
write_attribute(column.to_s, current_time) if respond_to?(column) && self.send(column).nil?
end
-
- timestamp_attributes_for_update_in_model.each do |column|
- write_attribute(column.to_s, current_time) if self.send(column).nil?
- end
end
super
end
def update(*args) #:nodoc:
- record_update_timestamps if !partial_updates? || changed?
+ if should_record_timestamps?
+ current_time = current_time_from_proper_timezone
+
+ timestamp_attributes_for_update_in_model.each do |column|
+ column = column.to_s
+ next if attribute_changed?(column)
+ write_attribute(column, current_time)
+ end
+ end
super
end
- def record_update_timestamps #:nodoc:
- return unless record_timestamps
- current_time = current_time_from_proper_timezone
- timestamp_attributes_for_update_in_model.inject({}) do |hash, column|
- hash[column.to_s] = write_attribute(column.to_s, current_time)
- hash
- end
+ def should_record_timestamps?
+ record_timestamps && !partial_updates? || changed?
end
- def timestamp_attributes_for_update_in_model #:nodoc:
- timestamp_attributes_for_update.select { |elem| respond_to?(elem) }
+ def timestamp_attributes_for_update_in_model
+ timestamp_attributes_for_update.select { |c| respond_to?(c) }
end
def timestamp_attributes_for_update #:nodoc:
@@ -78,9 +77,9 @@ module ActiveRecord
end
def all_timestamp_attributes #:nodoc:
- timestamp_attributes_for_update + timestamp_attributes_for_create
+ timestamp_attributes_for_create + timestamp_attributes_for_update
end
-
+
def current_time_from_proper_timezone #:nodoc:
self.class.default_timezone == :utc ? Time.now.utc : Time.now
end