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/association_preload.rb6
-rw-r--r--activerecord/lib/active_record/associations/association_collection.rb242
-rw-r--r--activerecord/lib/active_record/associations/association_proxy.rb113
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_association.rb1
-rw-r--r--activerecord/lib/active_record/associations/class_methods/join_dependency.rb2
-rw-r--r--activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb22
-rw-r--r--activerecord/lib/active_record/associations/has_many_association.rb15
-rw-r--r--activerecord/lib/active_record/associations/has_many_through_association.rb17
-rw-r--r--activerecord/lib/active_record/associations/has_one_association.rb5
-rw-r--r--activerecord/lib/active_record/associations/has_one_through_association.rb28
-rw-r--r--activerecord/lib/active_record/associations/singular_association.rb16
-rw-r--r--activerecord/lib/active_record/associations/through_association.rb219
-rw-r--r--activerecord/lib/active_record/attribute_methods/read.rb16
-rw-r--r--activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb5
-rw-r--r--activerecord/lib/active_record/autosave_association.rb2
-rw-r--r--activerecord/lib/active_record/base.rb80
-rw-r--r--activerecord/lib/active_record/callbacks.rb10
-rw-r--r--activerecord/lib/active_record/coders/yaml_column.rb39
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb258
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb90
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_adapter.rb5
-rw-r--r--activerecord/lib/active_record/connection_adapters/column.rb268
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb657
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql_adapter.rb114
-rw-r--r--activerecord/lib/active_record/fixtures.rb6
-rw-r--r--activerecord/lib/active_record/migration/command_recorder.rb20
-rw-r--r--activerecord/lib/active_record/session_store.rb2
-rw-r--r--activerecord/lib/active_record/timestamp.rb8
-rw-r--r--activerecord/lib/active_record/transactions.rb4
-rw-r--r--activerecord/lib/active_record/validations/uniqueness.rb6
31 files changed, 1559 insertions, 719 deletions
diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb
index cba4bab3ef..b83c00e9f8 100644
--- a/activerecord/lib/active_record/association_preload.rb
+++ b/activerecord/lib/active_record/association_preload.rb
@@ -125,7 +125,7 @@ module ActiveRecord
def add_preloaded_records_to_collection(parent_records, reflection_name, associated_record)
parent_records.each do |parent_record|
association_proxy = parent_record.send(reflection_name)
- association_proxy.loaded
+ association_proxy.loaded!
association_proxy.target.concat(Array.wrap(associated_record))
association_proxy.send(:set_inverse_instance, associated_record)
end
@@ -187,7 +187,7 @@ module ActiveRecord
id_to_record_map = construct_id_map(records)
- records.each {|record| record.send(reflection.name).loaded}
+ records.each { |record| record.send(reflection.name).loaded! }
options = reflection.options
right = Arel::Table.new(options[:join_table]).alias('t0')
@@ -268,7 +268,7 @@ module ActiveRecord
foreign_key = reflection.through_reflection_foreign_key
id_to_record_map = construct_id_map(records, foreign_key || reflection.options[:primary_key])
- records.each {|record| record.send(reflection.name).loaded}
+ records.each { |record| record.send(reflection.name).loaded! }
if options[:through]
through_records = preload_through_records(records, reflection, options[:through])
diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb
index b75e02c66b..2811f53424 100644
--- a/activerecord/lib/active_record/associations/association_collection.rb
+++ b/activerecord/lib/active_record/associations/association_collection.rb
@@ -22,8 +22,7 @@ module ActiveRecord
def select(select = nil)
if block_given?
- load_target
- @target.select.each { |e| yield e }
+ load_target.select.each { |e| yield e }
else
scoped.select(select)
end
@@ -33,45 +32,27 @@ module ActiveRecord
if @reflection.options[:finder_sql]
find_by_scan(*args)
else
- find_by_sql(*args)
+ scoped.find(*args)
end
end
- # Fetches the first one using SQL if possible.
def first(*args)
- if fetch_first_or_last_using_find?(args)
- find(:first, *args)
- else
- load_target unless loaded?
- args.shift if args.first.kind_of?(Hash) && args.first.empty?
- @target.first(*args)
- end
+ first_or_last(:first, *args)
end
- # Fetches the last one using SQL if possible.
def last(*args)
- if fetch_first_or_last_using_find?(args)
- find(:last, *args)
- else
- load_target unless loaded?
- @target.last(*args)
- end
+ first_or_last(:last, *args)
end
def to_ary
- load_target
- if @target.is_a?(Array)
- @target
- else
- Array.wrap(@target)
- end
+ load_target.dup
end
alias_method :to_a, :to_ary
def reset
- reset_target!
- reset_scopes_cache!
- @loaded = false
+ @_scopes_cache = {}
+ @loaded = false
+ @target = []
end
def build(attributes = {}, &block)
@@ -125,18 +106,35 @@ module ActiveRecord
#
# See delete for more info.
def delete_all
- load_target
- delete(@target)
- reset_target!
- reset_scopes_cache!
+ delete(load_target).tap do
+ reset
+ loaded!
+ end
+ end
+
+ # Identical to delete_all, except that the return value is the association (for chaining)
+ # rather than the records which have been removed.
+ def clear
+ delete_all
+ self
+ end
+
+ # Destroy all the records from this association.
+ #
+ # See destroy for more info.
+ def destroy_all
+ destroy(load_target).tap do
+ reset
+ loaded!
+ end
end
# Calculate sum using SQL, not Enumerable
def sum(*args)
if block_given?
- calculate(:sum, *args) { |*block_args| yield(*block_args) }
+ scoped.sum(*args) { |*block_args| yield(*block_args) }
else
- calculate(:sum, *args)
+ scoped.sum(*args)
end
end
@@ -155,7 +153,7 @@ module ActiveRecord
else
if @reflection.options[:uniq]
# This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL.
- column_name = "#{@reflection.quoted_table_name}.#{@reflection.klass.primary_key}" unless column_name
+ column_name ||= @reflection.klass.primary_key
options.merge!(:distinct => true)
end
@@ -200,30 +198,6 @@ module ActiveRecord
load_target
end
- # Removes all records from this association. Returns +self+ so method calls may be chained.
- def clear
- unless length.zero? # forces load_target if it hasn't happened already
- if @reflection.options[:dependent] == :destroy
- destroy_all
- else
- delete_all
- end
- end
-
- self
- end
-
- # Destroy all the records from this association.
- #
- # See destroy for more info.
- def destroy_all
- load_target
- destroy(@target).tap do
- reset_target!
- reset_scopes_cache!
- end
- end
-
def create(attrs = {})
if attrs.is_a?(Array)
attrs.collect { |attr| create(attr) }
@@ -283,7 +257,7 @@ module ActiveRecord
def any?
if block_given?
- method_missing(:any?) { |*block_args| yield(*block_args) }
+ load_target.any? { |*block_args| yield(*block_args) }
else
!empty?
end
@@ -292,7 +266,7 @@ module ActiveRecord
# Returns true if the collection has more than 1 record. Equivalent to collection.size > 1.
def many?
if block_given?
- method_missing(:many?) { |*block_args| yield(*block_args) }
+ load_target.many? { |*block_args| yield(*block_args) }
else
size > 1
end
@@ -309,13 +283,13 @@ module ActiveRecord
# This will perform a diff and delete/add only records that have changed.
def replace(other_array)
other_array.each { |val| raise_on_type_mismatch(val) }
-
- load_target
+ original_target = load_target.dup
transaction do
delete(@target - other_array)
unless concat(other_array - @target)
+ @target = original_target
raise RecordNotSaved, "Failed to replace #{@reflection.name} because one or more of the "
"new records could not be saved."
end
@@ -323,16 +297,40 @@ module ActiveRecord
end
def include?(record)
- return false unless record.is_a?(@reflection.klass)
- return include_in_memory?(record) if record.new_record?
- load_target if @reflection.options[:finder_sql] && !loaded?
- loaded? ? @target.include?(record) : exists?(record)
+ if record.is_a?(@reflection.klass)
+ if record.new_record?
+ include_in_memory?(record)
+ else
+ load_target if @reflection.options[:finder_sql]
+ loaded? ? @target.include?(record) : scoped.exists?(record)
+ end
+ else
+ false
+ end
end
def respond_to?(method, include_private = false)
super || @reflection.klass.respond_to?(method, include_private)
end
+ def method_missing(method, *args, &block)
+ match = DynamicFinderMatch.match(method)
+ if match && match.creator?
+ attributes = match.attribute_names
+ return send(:"find_by_#{attributes.join('_and_')}", *args) || create(Hash[attributes.zip(args)])
+ end
+
+ if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method))
+ super
+ elsif @reflection.klass.scopes[method]
+ @_scopes_cache ||= {}
+ @_scopes_cache[method] ||= {}
+ @_scopes_cache[method][args] ||= scoped.readonly(nil).send(method, *args)
+ else
+ scoped.readonly(nil).send(method, *args, &block)
+ end
+ end
+
protected
def association_scope
@@ -340,16 +338,8 @@ module ActiveRecord
super.apply_finder_options(options)
end
- def select_value
- super || uniq_select_value
- end
-
- def uniq_select_value
- @reflection.options[:uniq] && "DISTINCT #{@reflection.quoted_table_name}.*"
- end
-
def load_target
- if (!@owner.new_record? || foreign_key_present?) && !loaded?
+ if find_target?
targets = []
begin
@@ -361,26 +351,32 @@ module ActiveRecord
@target = merge_target_lists(targets, @target)
end
- loaded
+ loaded!
target
end
- def method_missing(method, *args, &block)
- match = DynamicFinderMatch.match(method)
- if match && match.creator?
- attributes = match.attribute_names
- return send(:"find_by_#{attributes.join('_and_')}", *args) || create(Hash[attributes.zip(args)])
- end
-
- if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method))
- super
- elsif @reflection.klass.scopes[method]
- @_scopes_cache ||= {}
- @_scopes_cache[method] ||= {}
- @_scopes_cache[method][args] ||= scoped.readonly(nil).send(method, *args)
+ def add_record_to_target_with_callbacks(record)
+ callback(:before_add, record)
+ yield(record) if block_given?
+ @target ||= [] unless loaded?
+ if @reflection.options[:uniq] && index = @target.index(record)
+ @target[index] = record
else
- scoped.readonly(nil).send(method, *args, &block)
+ @target << record
end
+ callback(:after_add, record)
+ set_inverse_instance(record)
+ record
+ end
+
+ private
+
+ def select_value
+ super || uniq_select_value
+ end
+
+ def uniq_select_value
+ @reflection.options[:uniq] && "DISTINCT #{@reflection.quoted_table_name}.*"
end
def custom_counter_sql
@@ -398,14 +394,6 @@ module ActiveRecord
interpolate_sql(@reflection.options[:finder_sql])
end
- def reset_target!
- @target = []
- end
-
- def reset_scopes_cache!
- @_scopes_cache = {}
- end
-
def find_target
records =
if @reflection.options[:finder_sql]
@@ -419,21 +407,6 @@ module ActiveRecord
records
end
- def add_record_to_target_with_callbacks(record)
- callback(:before_add, record)
- yield(record) if block_given?
- @target ||= [] unless loaded?
- if @reflection.options[:uniq] && index = @target.index(record)
- @target[index] = record
- else
- @target << record
- end
- callback(:after_add, record)
- set_inverse_instance(record)
- record
- end
-
- private
def merge_target_lists(loaded, existing)
return loaded if existing.empty?
return existing if loaded.empty?
@@ -466,17 +439,14 @@ module ActiveRecord
force ? record.save! : record.save(:validate => validate)
end
- def create_record(attrs, &block)
+ def create_record(attributes, &block)
ensure_owner_is_persisted!
-
- transaction do
- scoped.scoping { build_record(attrs, &block) }
- end
+ transaction { build_record(attributes, &block) }
end
- def build_record(attrs, &block)
- attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash)
- record = @reflection.build_association(attrs)
+ def build_record(attributes, &block)
+ attributes = scoped.scope_for_create.merge(attributes)
+ record = @reflection.build_association(attributes)
add_record_to_target_with_callbacks(record, &block)
end
@@ -516,9 +486,27 @@ module ActiveRecord
end
end
+ # Should we deal with assoc.first or assoc.last by issuing an independent query to
+ # the database, or by getting the target, and then taking the first/last item from that?
+ #
+ # If the args is just a non-empty options hash, go to the database.
+ #
+ # Otherwise, go to the database only if none of the following are true:
+ # * target already loaded
+ # * owner is new record
+ # * custom :finder_sql exists
+ # * target contains new or changed record(s)
+ # * the first arg is an integer (which indicates the number of records to be returned)
def fetch_first_or_last_using_find?(args)
- (args.first.kind_of?(Hash) && !args.first.empty?) || !(loaded? || @owner.new_record? || @reflection.options[:finder_sql] ||
- @target.any? { |record| record.new_record? } || args.first.kind_of?(Integer))
+ if args.first.is_a?(Hash)
+ true
+ else
+ !(loaded? ||
+ @owner.new_record? ||
+ @reflection.options[:finder_sql] ||
+ @target.any? { |record| record.new_record? || record.changed? } ||
+ args.first.kind_of?(Integer))
+ end
end
def include_in_memory?(record)
@@ -546,8 +534,12 @@ module ActiveRecord
end
end
- def find_by_sql(*args)
- scoped.find(*args)
+ # Fetches the first/last using SQL if possible, otherwise from the target array.
+ def first_or_last(type, *args)
+ args.shift if args.first.is_a?(Hash) && args.first.empty?
+
+ collection = fetch_first_or_last_using_find?(args) ? scoped : load_target
+ collection.send(type, *args)
end
end
end
diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb
index addc64cb42..07fff7f7d7 100644
--- a/activerecord/lib/active_record/associations/association_proxy.rb
+++ b/activerecord/lib/active_record/associations/association_proxy.rb
@@ -84,6 +84,16 @@ module ActiveRecord
super || (load_target && @target.respond_to?(*args))
end
+ # Forwards any missing method call to the \target.
+ def method_missing(method, *args, &block)
+ if load_target
+ return super unless @target.respond_to?(method)
+ @target.send(method, *args, &block)
+ end
+ rescue NoMethodError => e
+ raise e, e.message.sub(/ for #<.*$/, " via proxy for #{@target}")
+ end
+
# Forwards <tt>===</tt> explicitly to the \target because the instance method
# removal above doesn't catch it. Loads the \target if needed.
def ===(other)
@@ -98,13 +108,6 @@ module ActiveRecord
@reflection.klass.table_name
end
- # Returns the SQL string that corresponds to the <tt>:conditions</tt>
- # option of the macro, if given, or +nil+ otherwise.
- def conditions
- @conditions ||= interpolate_sql(@reflection.sanitized_conditions) if @reflection.sanitized_conditions
- end
- alias :sql_conditions :conditions
-
# Resets the \loaded flag to +false+ and sets the \target to +nil+.
def reset
@loaded = false
@@ -125,7 +128,7 @@ module ActiveRecord
end
# Asserts the \target has been loaded setting the \loaded flag to +true+.
- def loaded
+ def loaded!
@loaded = true
@stale_state = stale_state
end
@@ -149,7 +152,7 @@ module ActiveRecord
# Sets the target of this proxy to <tt>\target</tt>, and the \loaded flag to +true+.
def target=(target)
@target = target
- loaded
+ loaded!
end
# Forwards the call to the target. Loads the \target if needed.
@@ -167,14 +170,6 @@ module ActiveRecord
end
protected
- def interpolate_sql(sql, record = nil)
- @owner.send(:interpolate_sql, sql, record)
- end
-
- # Forwards the call to the reflection class.
- def sanitize_sql(sql, table_name = @reflection.klass.table_name)
- @reflection.klass.send(:sanitize_sql, sql, table_name)
- end
# Construct the scope for this association.
#
@@ -190,19 +185,13 @@ module ActiveRecord
scope = target_klass.unscoped
scope = scope.create_with(creation_attributes)
scope = scope.apply_finder_options(@reflection.options.slice(:conditions, :readonly, :include))
- scope = scope.select(select_value) if select_value = self.select_value
+ if select = select_value
+ scope = scope.select(select)
+ end
+ scope = scope.extending(*Array.wrap(@reflection.options[:extend]))
scope.where(construct_owner_conditions)
end
- def select_value
- @reflection.options[:select]
- end
-
- # Implemented by (some) subclasses
- def creation_attributes
- { }
- end
-
def aliased_table
target_klass.arel_table
end
@@ -227,6 +216,43 @@ module ActiveRecord
target_klass.scoped
end
+ # Loads the \target if needed and returns it.
+ #
+ # This method is abstract in the sense that it relies on +find_target+,
+ # which is expected to be provided by descendants.
+ #
+ # If the \target is already \loaded it is just returned. Thus, you can call
+ # +load_target+ unconditionally to get the \target.
+ #
+ # ActiveRecord::RecordNotFound is rescued within the method, and it is
+ # not reraised. The proxy is \reset and +nil+ is the return value.
+ def load_target
+ @target = find_target if find_target?
+ loaded!
+ target
+ rescue ActiveRecord::RecordNotFound
+ reset
+ end
+
+ private
+
+ def find_target?
+ !loaded? && (!@owner.new_record? || foreign_key_present?) && target_klass
+ end
+
+ def interpolate_sql(sql, record = nil)
+ @owner.send(:interpolate_sql, sql, record)
+ end
+
+ def select_value
+ @reflection.options[:select]
+ end
+
+ # Implemented by (some) subclasses
+ def creation_attributes
+ { }
+ end
+
# Returns a hash linking the owner to the association represented by the reflection
def construct_owner_attributes(reflection = @reflection)
attributes = {}
@@ -257,39 +283,6 @@ module ActiveRecord
end
end
- # Loads the \target if needed and returns it.
- #
- # This method is abstract in the sense that it relies on +find_target+,
- # which is expected to be provided by descendants.
- #
- # If the \target is already \loaded it is just returned. Thus, you can call
- # +load_target+ unconditionally to get the \target.
- #
- # ActiveRecord::RecordNotFound is rescued within the method, and it is
- # not reraised. The proxy is \reset and +nil+ is the return value.
- def load_target
- if !loaded? && (!@owner.new_record? || foreign_key_present?) && target_klass
- @target = find_target
- end
-
- loaded
- @target
- rescue ActiveRecord::RecordNotFound
- reset
- end
-
- private
-
- # Forwards any missing method call to the \target.
- def method_missing(method, *args, &block)
- if load_target
- return super unless @target.respond_to?(method)
- @target.send(method, *args, &block)
- end
- rescue NoMethodError => e
- raise e, e.message.sub(/ for #<.*$/, " via proxy for #{@target}")
- end
-
# Should be true if there is a foreign key present on the @owner which
# references the target. This is used to determine whether we can load
# the target if the @owner is currently a new record (and therefore
diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb
index e80b945dda..178c7204f8 100644
--- a/activerecord/lib/active_record/associations/belongs_to_association.rb
+++ b/activerecord/lib/active_record/associations/belongs_to_association.rb
@@ -19,6 +19,7 @@ module ActiveRecord
end
private
+
def update_counters(record)
counter_cache_name = @reflection.counter_cache_column
diff --git a/activerecord/lib/active_record/associations/class_methods/join_dependency.rb b/activerecord/lib/active_record/associations/class_methods/join_dependency.rb
index cb3edafab1..fdd4fe8946 100644
--- a/activerecord/lib/active_record/associations/class_methods/join_dependency.rb
+++ b/activerecord/lib/active_record/associations/class_methods/join_dependency.rb
@@ -210,7 +210,7 @@ module ActiveRecord
case macro
when :has_many, :has_and_belongs_to_many
collection = record.send(join_part.reflection.name)
- collection.loaded
+ collection.loaded!
collection.target.push(association)
collection.send(:set_inverse_instance, association)
when :belongs_to
diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb
index b28554dce1..3329a4af8e 100644
--- a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb
+++ b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb
@@ -5,17 +5,12 @@ module ActiveRecord
attr_reader :join_table
def initialize(owner, reflection)
- @join_table_name = reflection.options[:join_table]
- @join_table = Arel::Table.new(@join_table_name)
+ @join_table = Arel::Table.new(reflection.options[:join_table])
super
end
protected
- def count_records
- load_target.size
- end
-
def insert_record(record, force = true, validate = true)
if record.new_record?
return false unless save_record(record, force, validate)
@@ -35,6 +30,16 @@ module ActiveRecord
true
end
+ def association_scope
+ super.joins(construct_joins)
+ end
+
+ private
+
+ def count_records
+ load_target.size
+ end
+
def delete_records(records)
if sql = @reflection.options[:delete_sql]
records.each { |record| @owner.connection.delete(interpolate_sql(sql, record)) }
@@ -61,15 +66,10 @@ module ActiveRecord
super(join_table)
end
- def association_scope
- super.joins(construct_joins)
- end
-
def select_value
super || @reflection.klass.arel_table[Arel.star]
end
- private
def invertible_for?(record)
false
end
diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb
index b07441f3c6..caefd14ee3 100644
--- a/activerecord/lib/active_record/associations/has_many_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_association.rb
@@ -7,6 +7,14 @@ module ActiveRecord
# is provided by its child HasManyThroughAssociation.
class HasManyAssociation < AssociationCollection #:nodoc:
protected
+
+ def insert_record(record, force = false, validate = true)
+ set_owner_attributes(record)
+ save_record(record, force, validate)
+ end
+
+ private
+
# Returns the number of records in this collection.
#
# If the association has a counter cache it gets that value. Otherwise
@@ -32,7 +40,7 @@ module ActiveRecord
# If there's nothing in the database and @target has no new records
# we are certain the current target is an empty array. This is a
# documented side-effect of the method that may avoid an extra SELECT.
- @target ||= [] and loaded if count == 0
+ @target ||= [] and loaded! if count == 0
[@reflection.options[:limit], count].compact.min
end
@@ -45,11 +53,6 @@ module ActiveRecord
"#{@reflection.name}_count"
end
- def insert_record(record, force = false, validate = true)
- set_owner_attributes(record)
- save_record(record, force, validate)
- end
-
# Deletes the records according to the <tt>:dependent</tt> option.
def delete_records(records)
case @reflection.options[:dependent]
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 400db6baf1..d5b901beff 100644
--- a/activerecord/lib/active_record/associations/has_many_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_through_association.rb
@@ -30,13 +30,6 @@ module ActiveRecord
end
protected
- def target_reflection_has_associated_record?
- if @reflection.through_reflection.macro == :belongs_to && @owner[@reflection.through_reflection.foreign_key].blank?
- false
- else
- true
- end
- end
def insert_record(record, force = true, validate = true)
if record.new_record?
@@ -47,6 +40,16 @@ module ActiveRecord
through_association.create!(construct_join_attributes(record))
end
+ private
+
+ def target_reflection_has_associated_record?
+ if @reflection.through_reflection.macro == :belongs_to && @owner[@reflection.through_reflection.foreign_key].blank?
+ false
+ else
+ true
+ end
+ end
+
# TODO - add dependent option support
def delete_records(records)
through_association = @owner.send(@reflection.through_reflection.name)
diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb
index 6614cbbf18..a0828dcdea 100644
--- a/activerecord/lib/active_record/associations/has_one_association.rb
+++ b/activerecord/lib/active_record/associations/has_one_association.rb
@@ -26,11 +26,14 @@ module ActiveRecord
self.target = record
end
- private
+ protected
+
def association_scope
super.order(@reflection.options[:order])
end
+ private
+
alias creation_attributes construct_owner_attributes
# The reason that the save param for replace is false, if for create (not just build),
diff --git a/activerecord/lib/active_record/associations/has_one_through_association.rb b/activerecord/lib/active_record/associations/has_one_through_association.rb
index dcd74e7346..69771afe50 100644
--- a/activerecord/lib/active_record/associations/has_one_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_one_through_association.rb
@@ -11,24 +11,24 @@ module ActiveRecord
private
- def create_through_record(record)
- through_proxy = @owner.send(:association_proxy, @reflection.through_reflection.name)
- through_record = through_proxy.send(:load_target)
+ def create_through_record(record)
+ through_proxy = @owner.send(:association_proxy, @reflection.through_reflection.name)
+ through_record = through_proxy.send(:load_target)
- if through_record && !record
- through_record.destroy
- elsif record
- attributes = construct_join_attributes(record)
+ if through_record && !record
+ through_record.destroy
+ elsif record
+ attributes = construct_join_attributes(record)
- if through_record
- through_record.update_attributes(attributes)
- elsif @owner.new_record?
- through_proxy.build(attributes)
- else
- through_proxy.create(attributes)
+ if through_record
+ through_record.update_attributes(attributes)
+ elsif @owner.new_record?
+ through_proxy.build(attributes)
+ else
+ through_proxy.create(attributes)
+ end
end
end
- end
end
end
end
diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb
index b6f49c6f36..7f92d9712a 100644
--- a/activerecord/lib/active_record/associations/singular_association.rb
+++ b/activerecord/lib/active_record/associations/singular_association.rb
@@ -2,9 +2,7 @@ module ActiveRecord
module Associations
class SingularAssociation < AssociationProxy #:nodoc:
def create(attributes = {})
- record = scoped.scoping { @reflection.create_association(attributes) }
- set_new_record(record)
- record
+ new_record(:create, attributes)
end
def create!(attributes = {})
@@ -12,12 +10,11 @@ module ActiveRecord
end
def build(attributes = {})
- record = scoped.scoping { @reflection.build_association(attributes) }
- set_new_record(record)
- record
+ new_record(:build, attributes)
end
private
+
def find_target
scoped.first.tap { |record| set_inverse_instance(record) }
end
@@ -36,6 +33,13 @@ module ActiveRecord
raise_on_type_mismatch(record) if record
record
end
+
+ def new_record(method, attributes)
+ attributes = scoped.scope_for_create.merge(attributes || {})
+ record = @reflection.send("#{method}_association", attributes)
+ set_new_record(record)
+ record
+ end
end
end
end
diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb
index d2112fb2b6..c840a16160 100644
--- a/activerecord/lib/active_record/associations/through_association.rb
+++ b/activerecord/lib/active_record/associations/through_association.rb
@@ -5,140 +5,133 @@ module ActiveRecord
protected
- def target_scope
- super & @reflection.through_reflection.klass.scoped
- end
-
- def association_scope
- scope = super.joins(construct_joins).where(conditions)
- unless @reflection.options[:include]
- scope = scope.includes(@reflection.source_reflection.options[:include])
+ def target_scope
+ super & @reflection.through_reflection.klass.scoped
end
- scope
- end
-
- # This scope affects the creation of the associated records (not the join records). At the
- # moment we only support creating on a :through association when the source reflection is a
- # belongs_to. Thus it's not necessary to set a foreign key on the associated record(s), so
- # this scope has can legitimately be empty.
- def creation_attributes
- { }
- end
-
- def aliased_through_table
- name = @reflection.through_reflection.table_name
-
- @reflection.table_name == name ?
- @reflection.through_reflection.klass.arel_table.alias(name + "_join") :
- @reflection.through_reflection.klass.arel_table
- end
-
- def construct_owner_conditions
- super(aliased_through_table, @reflection.through_reflection)
- end
-
- def construct_joins
- right = aliased_through_table
- left = @reflection.klass.arel_table
-
- conditions = []
-
- if @reflection.source_reflection.macro == :belongs_to
- reflection_primary_key = @reflection.source_reflection.options[:primary_key] ||
- @reflection.klass.primary_key
- source_primary_key = @reflection.source_reflection.foreign_key
- if @reflection.options[:source_type]
- column = @reflection.source_reflection.foreign_type
- conditions <<
- right[column].eq(@reflection.options[:source_type])
- end
- else
- reflection_primary_key = @reflection.source_reflection.foreign_key
- source_primary_key = @reflection.source_reflection.options[:primary_key] ||
- @reflection.through_reflection.klass.primary_key
- if @reflection.source_reflection.options[:as]
- column = "#{@reflection.source_reflection.options[:as]}_type"
- conditions <<
- left[column].eq(@reflection.through_reflection.klass.name)
+
+ def association_scope
+ scope = super.joins(construct_joins)
+ scope = add_conditions(scope)
+ unless @reflection.options[:include]
+ scope = scope.includes(@reflection.source_reflection.options[:include])
end
+ scope
end
- conditions <<
- left[reflection_primary_key].eq(right[source_primary_key])
+ private
- right.create_join(
- right,
- right.create_on(right.create_and(conditions)))
- end
-
- # Construct attributes for :through pointing to owner and associate.
- def construct_join_attributes(associate)
- # TODO: revisit this to allow it for deletion, supposing dependent option is supported
- raise ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(@owner, @reflection) if [:has_one, :has_many].include?(@reflection.source_reflection.macro)
+ # This scope affects the creation of the associated records (not the join records). At the
+ # moment we only support creating on a :through association when the source reflection is a
+ # belongs_to. Thus it's not necessary to set a foreign key on the associated record(s), so
+ # this scope has can legitimately be empty.
+ def creation_attributes
+ { }
+ end
- join_attributes = {
- @reflection.source_reflection.foreign_key =>
- associate.send(@reflection.source_reflection.association_primary_key)
- }
+ def aliased_through_table
+ name = @reflection.through_reflection.table_name
- if @reflection.options[:source_type]
- join_attributes.merge!(@reflection.source_reflection.foreign_type => associate.class.base_class.name)
+ @reflection.table_name == name ?
+ @reflection.through_reflection.klass.arel_table.alias(name + "_join") :
+ @reflection.through_reflection.klass.arel_table
end
- if @reflection.through_reflection.options[:conditions].is_a?(Hash)
- join_attributes.merge!(@reflection.through_reflection.options[:conditions])
+ def construct_owner_conditions
+ super(aliased_through_table, @reflection.through_reflection)
end
- join_attributes
- end
+ def construct_joins
+ right = aliased_through_table
+ left = @reflection.klass.arel_table
+
+ conditions = []
+
+ if @reflection.source_reflection.macro == :belongs_to
+ reflection_primary_key = @reflection.source_reflection.association_primary_key
+ source_primary_key = @reflection.source_reflection.foreign_key
+
+ if @reflection.options[:source_type]
+ column = @reflection.source_reflection.foreign_type
+ conditions <<
+ right[column].eq(@reflection.options[:source_type])
+ end
+ else
+ reflection_primary_key = @reflection.source_reflection.foreign_key
+ source_primary_key = @reflection.source_reflection.active_record_primary_key
+
+ if @reflection.source_reflection.options[:as]
+ column = "#{@reflection.source_reflection.options[:as]}_type"
+ conditions <<
+ left[column].eq(@reflection.through_reflection.klass.name)
+ end
+ end
- def conditions
- @conditions = build_conditions unless defined?(@conditions)
- @conditions
- end
+ conditions <<
+ left[reflection_primary_key].eq(right[source_primary_key])
- def build_conditions
- through_conditions = build_through_conditions
- source_conditions = @reflection.source_reflection.options[:conditions]
- uses_sti = !@reflection.through_reflection.klass.descends_from_active_record?
+ right.create_join(
+ right,
+ right.create_on(right.create_and(conditions)))
+ end
- if through_conditions || source_conditions || uses_sti
- all = []
- all << interpolate_sql(sanitize_sql(source_conditions)) if source_conditions
- all << through_conditions if through_conditions
- all << build_sti_condition if uses_sti
+ # Construct attributes for :through pointing to owner and associate.
+ def construct_join_attributes(associate)
+ # TODO: revisit this to allow it for deletion, supposing dependent option is supported
+ raise ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(@owner, @reflection) if [:has_one, :has_many].include?(@reflection.source_reflection.macro)
- all.map { |sql| "(#{sql})" } * ' AND '
- end
- end
-
- def build_through_conditions
- conditions = @reflection.through_reflection.options[:conditions]
- if conditions.is_a?(Hash)
- interpolate_sql(@reflection.through_reflection.klass.send(:sanitize_sql, conditions)).gsub(
- @reflection.quoted_table_name,
- @reflection.through_reflection.quoted_table_name)
- elsif conditions
- interpolate_sql(sanitize_sql(conditions))
+ join_attributes = {
+ @reflection.source_reflection.foreign_key =>
+ associate.send(@reflection.source_reflection.association_primary_key)
+ }
+
+ if @reflection.options[:source_type]
+ join_attributes.merge!(@reflection.source_reflection.foreign_type => associate.class.base_class.name)
+ end
+
+ join_attributes
end
- end
- def build_sti_condition
- @reflection.through_reflection.klass.send(:type_condition).to_sql
- end
+ # The reason that we are operating directly on the scope here (rather than passing
+ # back some arel conditions to be added to the scope) is because scope.where([x, y])
+ # has a different meaning to scope.where(x).where(y) - the first version might
+ # perform some substitution if x is a string.
+ def add_conditions(scope)
+ unless @reflection.through_reflection.klass.descends_from_active_record?
+ scope = scope.where(@reflection.through_reflection.klass.send(:type_condition))
+ end
- alias_method :sql_conditions, :conditions
+ scope = scope.where(@reflection.source_reflection.options[:conditions])
+ scope.where(through_conditions)
+ end
- def stale_state
- if @reflection.through_reflection.macro == :belongs_to
- @owner[@reflection.through_reflection.foreign_key].to_s
+ # If there is a hash of conditions then we make sure the keys are scoped to the
+ # through table name if left ambiguous.
+ def through_conditions
+ conditions = @reflection.through_reflection.options[:conditions]
+
+ if conditions.is_a?(Hash)
+ Hash[conditions.map { |key, value|
+ unless value.is_a?(Hash) || key.to_s.include?('.')
+ key = aliased_through_table.name + '.' + key.to_s
+ end
+
+ [key, value]
+ }]
+ else
+ conditions
+ end
end
- end
- def foreign_key_present?
- @reflection.through_reflection.macro == :belongs_to &&
- !@owner[@reflection.through_reflection.foreign_key].nil?
- end
+ def stale_state
+ if @reflection.through_reflection.macro == :belongs_to
+ @owner[@reflection.through_reflection.foreign_key].to_s
+ end
+ end
+
+ def foreign_key_present?
+ @reflection.through_reflection.macro == :belongs_to &&
+ !@owner[@reflection.through_reflection.foreign_key].nil?
+ end
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb
index 660fa9a564..ab86d8bad1 100644
--- a/activerecord/lib/active_record/attribute_methods/read.rb
+++ b/activerecord/lib/active_record/attribute_methods/read.rb
@@ -54,14 +54,14 @@ module ActiveRecord
# Define read method for serialized attribute.
def define_read_method_for_serialized_attribute(attr_name)
- access_code = "@attributes_cache['#{attr_name}'] ||= unserialize_attribute('#{attr_name}')"
+ access_code = "@attributes_cache['#{attr_name}'] ||= @attributes['#{attr_name}']"
generated_attribute_methods.module_eval("def _#{attr_name}; #{access_code}; end; alias #{attr_name} _#{attr_name}", __FILE__, __LINE__)
end
# Define an attribute reader method. Cope with nil column.
def define_read_method(symbol, attr_name, column)
- cast_code = column.type_cast_code('v') if column
- access_code = cast_code ? "(v=@attributes['#{attr_name}']) && #{cast_code}" : "@attributes['#{attr_name}']"
+ cast_code = column.type_cast_code('v')
+ access_code = "(v=@attributes['#{attr_name}']) && #{cast_code}"
unless attr_name.to_s == self.primary_key.to_s
access_code.insert(0, "missing_attribute('#{attr_name}', caller) unless @attributes.has_key?('#{attr_name}'); ")
@@ -106,14 +106,10 @@ module ActiveRecord
# Returns the unserialized object of the attribute.
def unserialize_attribute(attr_name)
- unserialized_object = object_from_yaml(@attributes[attr_name])
+ coder = self.class.serialized_attributes[attr_name]
+ unserialized_object = coder.load(@attributes[attr_name])
- if unserialized_object.is_a?(self.class.serialized_attributes[attr_name]) || unserialized_object.nil?
- @attributes.frozen? ? unserialized_object : @attributes[attr_name] = unserialized_object
- else
- raise SerializationTypeMismatch,
- "#{attr_name} was supposed to be a #{self.class.serialized_attributes[attr_name]}, but was a #{unserialized_object.class.to_s}"
- end
+ @attributes.frozen? ? unserialized_object : @attributes[attr_name] = unserialized_object
end
private
diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
index dc2785b6bf..a72eecb50e 100644
--- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
+++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
@@ -40,12 +40,13 @@ module ActiveRecord
def define_method_attribute=(attr_name)
if create_time_zone_conversion_attribute?(attr_name, columns_hash[attr_name])
method_body, line = <<-EOV, __LINE__ + 1
- def #{attr_name}=(time)
+ def #{attr_name}=(original_time)
+ time = original_time.dup
unless time.acts_like?(:time)
time = time.is_a?(String) ? Time.zone.parse(time) : time.to_time rescue time
end
time = time.in_time_zone rescue nil if time
- write_attribute(:#{attr_name}, time)
+ write_attribute(:#{attr_name}, (time || original_time))
end
EOV
generated_attribute_methods.module_eval(method_body, __FILE__, line)
diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb
index 70ed16eeaf..9c7bb67479 100644
--- a/activerecord/lib/active_record/autosave_association.rb
+++ b/activerecord/lib/active_record/autosave_association.rb
@@ -368,7 +368,7 @@ module ActiveRecord
if association.updated?
association_id = association.send(reflection.options[:primary_key] || :id)
self[reflection.foreign_key] = association_id
- association.loaded
+ association.loaded!
end
saved if autosave
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index e444e607d6..0941700803 100644
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -1,3 +1,8 @@
+begin
+ require 'psych'
+rescue LoadError
+end
+
require 'yaml'
require 'set'
require 'active_support/benchmarkable'
@@ -531,7 +536,13 @@ module ActiveRecord #:nodoc:
# serialize :preferences
# end
def serialize(attr_name, class_name = Object)
- serialized_attributes[attr_name.to_s] = class_name
+ coder = if [:load, :dump].all? { |x| class_name.respond_to?(x) }
+ class_name
+ else
+ Coders::YAMLColumn.new(class_name)
+ end
+
+ serialized_attributes[attr_name.to_s] = coder
end
# Guesses the table name (in forced lower-case) based on the name of the class in the
@@ -657,16 +668,16 @@ module ActiveRecord #:nodoc:
# Returns an array of column objects for the table associated with this class.
def columns
- unless defined?(@columns) && @columns
- @columns = connection.columns(table_name, "#{name} Columns")
- @columns.each { |column| column.primary = column.name == primary_key }
- end
- @columns
+ @@columns[table_name] ||= connection.columns(
+ table_name, "#{name} Columns"
+ ).tap { |columns|
+ columns.each { |column| column.primary = column.name == primary_key }
+ }
end
# Returns a hash of column objects for the table associated with this class.
def columns_hash
- @columns_hash ||= Hash[columns.map { |column| [column.name, column] }]
+ @@columns_hash[table_name] ||= Hash[columns.map { |column| [column.name, column] }]
end
# Returns an array of column names as strings.
@@ -723,10 +734,16 @@ module ActiveRecord #:nodoc:
def reset_column_information
connection.clear_cache!
undefine_attribute_methods
- @column_names = @columns = @columns_hash = @content_columns = @dynamic_methods_hash = @inheritance_column = nil
+ reset_column_cache
+ @column_names = @content_columns = @dynamic_methods_hash = @inheritance_column = nil
@arel_engine = @relation = @arel_table = nil
end
+ def reset_column_cache # :nodoc:
+ @@columns.delete table_name
+ @@columns_hash.delete table_name
+ end
+
def attribute_method?(attribute)
super || (table_exists? && column_names.include?(attribute.to_s.sub(/=$/, '')))
end
@@ -1371,6 +1388,8 @@ MSG
quoted_value
end
end
+ @@columns_hash = {}
+ @@columns = {}
public
# New objects can be instantiated as either empty (pass no construction parameter) or pre-set with
@@ -1395,7 +1414,7 @@ MSG
self.attributes = attributes unless attributes.nil?
result = yield self if block_given?
- _run_initialize_callbacks
+ run_callbacks :initialize
result
end
@@ -1427,13 +1446,19 @@ MSG
# post.title # => 'hello world'
def init_with(coder)
@attributes = coder['attributes']
+
+ (@attributes.keys & self.class.serialized_attributes.keys).each do |key|
+ coder = self.class.serialized_attributes[key]
+ @attributes[key] = coder.load @attributes[key]
+ end
+
@attributes_cache, @previously_changed, @changed_attributes = {}, {}, {}
@association_cache = {}
@aggregation_cache = {}
@readonly = @destroyed = @marked_for_destruction = false
@new_record = false
- _run_find_callbacks
- _run_initialize_callbacks
+ run_callbacks :find
+ run_callbacks :initialize
end
# Specifies how the record is dumped by +Marshal+.
@@ -1720,17 +1745,25 @@ MSG
# Returns a copy of the attributes hash where all the values have been safely quoted for use in
# an Arel insert/update method.
def arel_attributes_values(include_primary_key = true, include_readonly_attributes = true, attribute_names = @attributes.keys)
- attrs = {}
+ attrs = {}
+ klass = self.class
+ arel_table = klass.arel_table
+
attribute_names.each do |name|
if (column = column_for_attribute(name)) && (include_primary_key || !column.primary)
if include_readonly_attributes || (!include_readonly_attributes && !self.class.readonly_attributes.include?(name))
- value = read_attribute(name)
- if !value.nil? && self.class.serialized_attributes.key?(name)
- value = YAML.dump value
- end
- attrs[self.class.arel_table[name]] = value
+ value = if coder = klass.serialized_attributes[name]
+ coder.dump @attributes[name]
+ else
+ # FIXME: we need @attributes to be used consistently.
+ # If the values stored in @attributes were already type
+ # casted, this code could be simplified
+ read_attribute(name)
+ end
+
+ attrs[arel_table[name]] = value
end
end
end
@@ -1854,11 +1887,6 @@ MSG
end
end
- def object_from_yaml(string)
- return string unless string.is_a?(String) && string =~ /^---/
- YAML::load(string) rescue string
- end
-
def populate_with_current_scope_attributes
if scope = self.class.send(:current_scoped_methods)
create_with = scope.scope_for_create
@@ -1870,11 +1898,9 @@ MSG
# Clear attributes and changed_attributes
def clear_timestamp_attributes
- %w(created_at created_on updated_at updated_on).each do |attribute_name|
- if has_attribute?(attribute_name)
- self[attribute_name] = nil
- changed_attributes.delete(attribute_name)
- end
+ all_timestamp_attributes_in_model.each do |attribute_name|
+ self[attribute_name] = nil
+ changed_attributes.delete(attribute_name)
end
end
end
diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb
index 87b77313bb..ff4ce1b605 100644
--- a/activerecord/lib/active_record/callbacks.rb
+++ b/activerecord/lib/active_record/callbacks.rb
@@ -233,25 +233,25 @@ module ActiveRecord
end
def destroy #:nodoc:
- _run_destroy_callbacks { super }
+ run_callbacks(:destroy) { super }
end
def touch(*) #:nodoc:
- _run_touch_callbacks { super }
+ run_callbacks(:touch) { super }
end
private
def create_or_update #:nodoc:
- _run_save_callbacks { super }
+ run_callbacks(:save) { super }
end
def create #:nodoc:
- _run_create_callbacks { super }
+ run_callbacks(:create) { super }
end
def update(*) #:nodoc:
- _run_update_callbacks { super }
+ run_callbacks(:update) { super }
end
end
end
diff --git a/activerecord/lib/active_record/coders/yaml_column.rb b/activerecord/lib/active_record/coders/yaml_column.rb
new file mode 100644
index 0000000000..fcecc11aba
--- /dev/null
+++ b/activerecord/lib/active_record/coders/yaml_column.rb
@@ -0,0 +1,39 @@
+module ActiveRecord
+ # :stopdoc:
+ module Coders
+ class YAMLColumn
+ RESCUE_ERRORS = [ ArgumentError ]
+
+ if defined?(Psych) && defined?(Psych::SyntaxError)
+ RESCUE_ERRORS << Psych::SyntaxError
+ end
+
+ attr_accessor :object_class
+
+ def initialize(object_class = Object)
+ @object_class = object_class
+ end
+
+ def dump(obj)
+ YAML.dump obj
+ end
+
+ def load(yaml)
+ return yaml unless yaml.is_a?(String) && yaml =~ /^---/
+ begin
+ obj = YAML.load(yaml)
+
+ unless obj.is_a?(object_class) || obj.nil?
+ raise SerializationTypeMismatch,
+ "Attribute was supposed to be a #{object_class}, but was a #{obj.class}"
+ end
+
+ obj
+ rescue *RESCUE_ERRORS
+ yaml
+ end
+ end
+ end
+ end
+ # :startdoc
+end
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 cffa2387de..54f70c59f8 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
@@ -212,7 +212,7 @@ module ActiveRecord
# calling +checkout+ on this pool.
def checkin(conn)
@connection_mutex.synchronize do
- conn.send(:_run_checkin_callbacks) do
+ conn.run_callbacks :checkin do
@checked_out.delete conn
@queue.signal
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
index 60ccf9edf3..7ac48c6646 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
@@ -6,264 +6,6 @@ require 'bigdecimal/util'
module ActiveRecord
module ConnectionAdapters #:nodoc:
- # An abstract definition of a column in a table.
- class Column
- TRUE_VALUES = [true, 1, '1', 't', 'T', 'true', 'TRUE'].to_set
- FALSE_VALUES = [false, 0, '0', 'f', 'F', 'false', 'FALSE'].to_set
-
- module Format
- ISO_DATE = /\A(\d{4})-(\d\d)-(\d\d)\z/
- ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/
- end
-
- attr_reader :name, :default, :type, :limit, :null, :sql_type, :precision, :scale
- attr_accessor :primary
-
- # Instantiates a new column in the table.
- #
- # +name+ is the column's name, such as <tt>supplier_id</tt> in <tt>supplier_id int(11)</tt>.
- # +default+ is the type-casted default value, such as +new+ in <tt>sales_stage varchar(20) default 'new'</tt>.
- # +sql_type+ is used to extract the column's length, if necessary. For example +60+ in
- # <tt>company_name varchar(60)</tt>.
- # It will be mapped to one of the standard Rails SQL types in the <tt>type</tt> attribute.
- # +null+ determines if this column allows +NULL+ values.
- def initialize(name, default, sql_type = nil, null = true)
- @name, @sql_type, @null = name, sql_type, null
- @limit, @precision, @scale = extract_limit(sql_type), extract_precision(sql_type), extract_scale(sql_type)
- @type = simplified_type(sql_type)
- @default = extract_default(default)
-
- @primary = nil
- end
-
- # Returns +true+ if the column is either of type string or text.
- def text?
- type == :string || type == :text
- end
-
- # Returns +true+ if the column is either of type integer, float or decimal.
- def number?
- type == :integer || type == :float || type == :decimal
- end
-
- def has_default?
- !default.nil?
- 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
-
- # Casts value (which is a String) to an appropriate instance.
- 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
- when :decimal then self.class.value_to_decimal(value)
- when :datetime then self.class.string_to_time(value)
- when :timestamp then self.class.string_to_time(value)
- when :time then self.class.string_to_dummy_time(value)
- when :date then self.class.string_to_date(value)
- when :binary then self.class.binary_to_string(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 then "#{self.class.name}.string_to_time(#{var_name})"
- when :timestamp then "#{self.class.name}.string_to_time(#{var_name})"
- when :time then "#{self.class.name}.string_to_dummy_time(#{var_name})"
- when :date then "#{self.class.name}.string_to_date(#{var_name})"
- when :binary then "#{self.class.name}.binary_to_string(#{var_name})"
- when :boolean then "#{self.class.name}.value_to_boolean(#{var_name})"
- else nil
- end
- end
-
- # Returns the human name of the column name.
- #
- # ===== Examples
- # Column.new('sales_stage', ...).human_name # => 'Sales stage'
- def human_name
- Base.human_attribute_name(@name)
- end
-
- def extract_default(default)
- type_cast(default)
- end
-
- # Used to convert from Strings to BLOBs
- def string_to_binary(value)
- self.class.string_to_binary(value)
- end
-
- class << self
- # Used to convert from Strings to BLOBs
- def string_to_binary(value)
- value
- end
-
- # Used to convert from BLOBs to Strings
- def binary_to_string(value)
- value
- end
-
- def string_to_date(string)
- return string unless string.is_a?(String)
- return nil if string.empty?
-
- fast_string_to_date(string) || fallback_string_to_date(string)
- end
-
- def string_to_time(string)
- return string unless string.is_a?(String)
- return nil if string.empty?
-
- fast_string_to_time(string) || fallback_string_to_time(string)
- end
-
- def string_to_dummy_time(string)
- return string unless string.is_a?(String)
- return nil if string.empty?
-
- string_to_time "2000-01-01 #{string}"
- end
-
- # convert something to a boolean
- def value_to_boolean(value)
- if value.is_a?(String) && value.blank?
- nil
- else
- TRUE_VALUES.include?(value)
- end
- end
-
- # convert something to a BigDecimal
- def value_to_decimal(value)
- # Using .class is faster than .is_a? and
- # subclasses of BigDecimal will be handled
- # in the else clause
- if value.class == BigDecimal
- value
- elsif value.respond_to?(:to_d)
- value.to_d
- else
- value.to_s.to_d
- end
- end
-
- protected
- # '0.123456' -> 123456
- # '1.123456' -> 123456
- def microseconds(time)
- ((time[:sec_fraction].to_f % 1) * 1_000_000).to_i
- end
-
- def new_date(year, mon, mday)
- if year && year != 0
- Date.new(year, mon, mday) rescue nil
- end
- end
-
- def new_time(year, mon, mday, hour, min, sec, microsec)
- # Treat 0000-00-00 00:00:00 as nil.
- return nil if year.nil? || year == 0
-
- Time.time_with_datetime_fallback(Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil
- end
-
- def fast_string_to_date(string)
- if string =~ Format::ISO_DATE
- new_date $1.to_i, $2.to_i, $3.to_i
- end
- end
-
- # Doesn't handle time zones.
- def fast_string_to_time(string)
- if string =~ Format::ISO_DATETIME
- microsec = ($7.to_f * 1_000_000).to_i
- new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec
- end
- end
-
- def fallback_string_to_date(string)
- new_date(*::Date._parse(string, false).values_at(:year, :mon, :mday))
- end
-
- def fallback_string_to_time(string)
- time_hash = Date._parse(string)
- time_hash[:sec_fraction] = microseconds(time_hash)
-
- new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction))
- end
- end
-
- private
- def extract_limit(sql_type)
- $1.to_i if sql_type =~ /\((.*)\)/
- end
-
- def extract_precision(sql_type)
- $2.to_i if sql_type =~ /^(numeric|decimal|number)\((\d+)(,\d+)?\)/i
- end
-
- def extract_scale(sql_type)
- case sql_type
- when /^(numeric|decimal|number)\((\d+)\)/i then 0
- when /^(numeric|decimal|number)\((\d+)(,(\d+))\)/i then $4.to_i
- end
- end
-
- def simplified_type(field_type)
- case field_type
- when /int/i
- :integer
- when /float|double/i
- :float
- when /decimal|numeric|number/i
- extract_scale(field_type) == 0 ? :integer : :decimal
- when /datetime/i
- :datetime
- when /timestamp/i
- :timestamp
- when /time/i
- :time
- when /date/i
- :date
- when /clob/i, /text/i
- :text
- when /blob/i, /binary/i
- :binary
- when /char/i, /string/i
- :string
- when /boolean/i
- :boolean
- end
- end
- end
-
class IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths) #:nodoc:
end
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 5b9c48bafa..3ec7dd02a4 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
@@ -176,6 +176,13 @@ module ActiveRecord
# # Other column alterations here
# end
#
+ # The +options+ hash can include the following keys:
+ # [<tt>:bulk</tt>]
+ # Set this to true to make this a bulk alter query, such as
+ # ALTER TABLE `users` ADD COLUMN age INT(11), ADD COLUMN birthdate DATETIME ...
+ #
+ # Defaults to false.
+ #
# ===== Examples
# ====== Add a column
# change_table(:suppliers) do |t|
@@ -224,8 +231,14 @@ module ActiveRecord
#
# See also Table for details on
# all of the various column transformation
- def change_table(table_name)
- yield Table.new(table_name, self)
+ def change_table(table_name, options = {})
+ if supports_bulk_alter? && options[:bulk]
+ recorder = ActiveRecord::Migration::CommandRecorder.new(self)
+ yield Table.new(table_name, recorder)
+ bulk_change_table(table_name, recorder.commands)
+ else
+ yield Table.new(table_name, self)
+ end
end
# Renames a table.
@@ -253,10 +266,7 @@ module ActiveRecord
# remove_column(:suppliers, :qualification)
# remove_columns(:suppliers, :qualification, :experience)
def remove_column(table_name, *column_names)
- raise ArgumentError.new("You must specify at least one column name. Example: remove_column(:people, :first_name)") if column_names.empty?
- column_names.flatten.each do |column_name|
- execute "ALTER TABLE #{quote_table_name(table_name)} DROP #{quote_column_name(column_name)}"
- end
+ columns_for_remove(table_name, *column_names).each {|column_name| execute "ALTER TABLE #{quote_table_name(table_name)} DROP #{column_name}" }
end
alias :remove_columns :remove_column
@@ -327,25 +337,8 @@ module ActiveRecord
#
# Note: SQLite doesn't support index length
def add_index(table_name, column_name, options = {})
- column_names = Array.wrap(column_name)
- index_name = index_name(table_name, :column => column_names)
-
- if Hash === options # legacy support, since this param was a string
- index_type = options[:unique] ? "UNIQUE" : ""
- index_name = options[:name].to_s if options.key?(:name)
- else
- index_type = options
- end
-
- if index_name.length > index_name_length
- raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' is too long; the limit is #{index_name_length} characters"
- end
- if index_name_exists?(table_name, index_name, false)
- raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' already exists"
- end
- quoted_column_names = quoted_columns_for_index(column_names, options).join(", ")
-
- execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} (#{quoted_column_names})"
+ index_name, index_type, index_columns = add_index_options(table_name, column_name, options)
+ execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} (#{index_columns})"
end
# Remove the given index from the table.
@@ -359,11 +352,7 @@ module ActiveRecord
# Remove the index named by_branch_party in the accounts table.
# remove_index :accounts, :name => :by_branch_party
def remove_index(table_name, options = {})
- index_name = index_name(table_name, options)
- unless index_name_exists?(table_name, index_name, true)
- raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' does not exist"
- end
- remove_index!(table_name, index_name)
+ remove_index!(table_name, index_name_for_remove(table_name, options))
end
def remove_index!(table_name, index_name) #:nodoc:
@@ -469,7 +458,7 @@ module ActiveRecord
end
def type_to_sql(type, limit = nil, precision = nil, scale = nil) #:nodoc:
- if native = native_database_types[type]
+ if native = native_database_types[type.to_sym]
column_type_sql = (native.is_a?(Hash) ? native[:name] : native).dup
if type == :decimal # ignore limit, use precision and scale
@@ -537,6 +526,45 @@ module ActiveRecord
options.include?(:default) && !(options[:null] == false && options[:default].nil?)
end
+ def add_index_options(table_name, column_name, options = {})
+ column_names = Array.wrap(column_name)
+ index_name = index_name(table_name, :column => column_names)
+
+ if Hash === options # legacy support, since this param was a string
+ index_type = options[:unique] ? "UNIQUE" : ""
+ index_name = options[:name].to_s if options.key?(:name)
+ else
+ index_type = options
+ end
+
+ if index_name.length > index_name_length
+ raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' is too long; the limit is #{index_name_length} characters"
+ end
+ if index_name_exists?(table_name, index_name, false)
+ raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' already exists"
+ end
+ index_columns = quoted_columns_for_index(column_names, options).join(", ")
+
+ [index_name, index_type, index_columns]
+ end
+
+ def index_name_for_remove(table_name, options = {})
+ index_name = index_name(table_name, options)
+
+ unless index_name_exists?(table_name, index_name, true)
+ raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' does not exist"
+ end
+
+ index_name
+ end
+
+ def columns_for_remove(table_name, *column_names)
+ column_names = column_names.flatten
+
+ raise ArgumentError.new("You must specify at least one column name. Example: remove_column(:people, :first_name)") if column_names.blank?
+ column_names.map {|column_name| quote_column_name(column_name) }
+ end
+
private
def table_definition
TableDefinition.new(self)
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
index 5ff5813699..3a3a73fc42 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'
# TODO: Autoload these files
+require 'active_record/connection_adapters/column'
require 'active_record/connection_adapters/abstract/schema_definitions'
require 'active_record/connection_adapters/abstract/schema_statements'
require 'active_record/connection_adapters/abstract/database_statements'
@@ -77,6 +78,10 @@ module ActiveRecord
false
end
+ def supports_bulk_alter?
+ false
+ end
+
# Does this adapter support savepoints? PostgreSQL and MySQL do,
# SQLite < 3.6.8 does not.
def supports_savepoints?
diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb
new file mode 100644
index 0000000000..beb06ea622
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/column.rb
@@ -0,0 +1,268 @@
+module ActiveRecord
+ # :stopdoc:
+ module ConnectionAdapters
+ # An abstract definition of a column in a table.
+ class Column
+ TRUE_VALUES = [true, 1, '1', 't', 'T', 'true', 'TRUE'].to_set
+ FALSE_VALUES = [false, 0, '0', 'f', 'F', 'false', 'FALSE'].to_set
+
+ module Format
+ ISO_DATE = /\A(\d{4})-(\d\d)-(\d\d)\z/
+ ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/
+ end
+
+ attr_reader :name, :default, :type, :limit, :null, :sql_type, :precision, :scale
+ attr_accessor :primary, :coder
+
+ alias :encoded? :coder
+
+ # Instantiates a new column in the table.
+ #
+ # +name+ is the column's name, such as <tt>supplier_id</tt> in <tt>supplier_id int(11)</tt>.
+ # +default+ is the type-casted default value, such as +new+ in <tt>sales_stage varchar(20) default 'new'</tt>.
+ # +sql_type+ is used to extract the column's length, if necessary. For example +60+ in
+ # <tt>company_name varchar(60)</tt>.
+ # It will be mapped to one of the standard Rails SQL types in the <tt>type</tt> attribute.
+ # +null+ determines if this column allows +NULL+ values.
+ def initialize(name, default, sql_type = nil, null = true)
+ @name = name
+ @sql_type = sql_type
+ @null = null
+ @limit = extract_limit(sql_type)
+ @precision = extract_precision(sql_type)
+ @scale = extract_scale(sql_type)
+ @type = simplified_type(sql_type)
+ @default = extract_default(default)
+ @primary = nil
+ @coder = nil
+ end
+
+ # Returns +true+ if the column is either of type string or text.
+ def text?
+ type == :string || type == :text
+ end
+
+ # Returns +true+ if the column is either of type integer, float or decimal.
+ def number?
+ type == :integer || type == :float || type == :decimal
+ end
+
+ def has_default?
+ !default.nil?
+ 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, :timestamp, :time then Time
+ when :date then Date
+ when :text, :string, :binary then String
+ when :boolean then Object
+ end
+ end
+
+ # Casts value (which is a String) to an appropriate instance.
+ def type_cast(value)
+ return nil if value.nil?
+ return coder.load(value) if encoded?
+
+ klass = self.class
+
+ case type
+ when :string, :text then value
+ when :integer then value.to_i rescue value ? 1 : 0
+ when :float then value.to_f
+ when :decimal then klass.value_to_decimal(value)
+ when :datetime, :timestamp then klass.string_to_time(value)
+ when :time then klass.string_to_dummy_time(value)
+ when :date then klass.string_to_date(value)
+ when :binary then klass.binary_to_string(value)
+ when :boolean then klass.value_to_boolean(value)
+ else value
+ end
+ end
+
+ def type_cast_code(var_name)
+ klass = self.class.name
+
+ case type
+ when :string, :text then var_name
+ when :integer then "(#{var_name}.to_i rescue #{var_name} ? 1 : 0)"
+ when :float then "#{var_name}.to_f"
+ when :decimal then "#{klass}.value_to_decimal(#{var_name})"
+ when :datetime, :timestamp then "#{klass}.string_to_time(#{var_name})"
+ when :time then "#{klass}.string_to_dummy_time(#{var_name})"
+ when :date then "#{klass}.string_to_date(#{var_name})"
+ when :binary then "#{klass}.binary_to_string(#{var_name})"
+ when :boolean then "#{klass}.value_to_boolean(#{var_name})"
+ else var_name
+ end
+ end
+
+ # Returns the human name of the column name.
+ #
+ # ===== Examples
+ # Column.new('sales_stage', ...).human_name # => 'Sales stage'
+ def human_name
+ Base.human_attribute_name(@name)
+ end
+
+ def extract_default(default)
+ type_cast(default)
+ end
+
+ # Used to convert from Strings to BLOBs
+ def string_to_binary(value)
+ self.class.string_to_binary(value)
+ end
+
+ class << self
+ # Used to convert from Strings to BLOBs
+ def string_to_binary(value)
+ value
+ end
+
+ # Used to convert from BLOBs to Strings
+ def binary_to_string(value)
+ value
+ end
+
+ def string_to_date(string)
+ return string unless string.is_a?(String)
+ return nil if string.empty?
+
+ fast_string_to_date(string) || fallback_string_to_date(string)
+ end
+
+ def string_to_time(string)
+ return string unless string.is_a?(String)
+ return nil if string.empty?
+
+ fast_string_to_time(string) || fallback_string_to_time(string)
+ end
+
+ def string_to_dummy_time(string)
+ return string unless string.is_a?(String)
+ return nil if string.empty?
+
+ string_to_time "2000-01-01 #{string}"
+ end
+
+ # convert something to a boolean
+ def value_to_boolean(value)
+ if value.is_a?(String) && value.blank?
+ nil
+ else
+ TRUE_VALUES.include?(value)
+ end
+ end
+
+ # convert something to a BigDecimal
+ def value_to_decimal(value)
+ # Using .class is faster than .is_a? and
+ # subclasses of BigDecimal will be handled
+ # in the else clause
+ if value.class == BigDecimal
+ value
+ elsif value.respond_to?(:to_d)
+ value.to_d
+ else
+ value.to_s.to_d
+ end
+ end
+
+ protected
+ # '0.123456' -> 123456
+ # '1.123456' -> 123456
+ def microseconds(time)
+ ((time[:sec_fraction].to_f % 1) * 1_000_000).to_i
+ end
+
+ def new_date(year, mon, mday)
+ if year && year != 0
+ Date.new(year, mon, mday) rescue nil
+ end
+ end
+
+ def new_time(year, mon, mday, hour, min, sec, microsec)
+ # Treat 0000-00-00 00:00:00 as nil.
+ return nil if year.nil? || year == 0
+
+ Time.time_with_datetime_fallback(Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil
+ end
+
+ def fast_string_to_date(string)
+ if string =~ Format::ISO_DATE
+ new_date $1.to_i, $2.to_i, $3.to_i
+ end
+ end
+
+ # Doesn't handle time zones.
+ def fast_string_to_time(string)
+ if string =~ Format::ISO_DATETIME
+ microsec = ($7.to_f * 1_000_000).to_i
+ new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec
+ end
+ end
+
+ def fallback_string_to_date(string)
+ new_date(*::Date._parse(string, false).values_at(:year, :mon, :mday))
+ end
+
+ def fallback_string_to_time(string)
+ time_hash = Date._parse(string)
+ time_hash[:sec_fraction] = microseconds(time_hash)
+
+ new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction))
+ end
+ end
+
+ private
+ def extract_limit(sql_type)
+ $1.to_i if sql_type =~ /\((.*)\)/
+ end
+
+ def extract_precision(sql_type)
+ $2.to_i if sql_type =~ /^(numeric|decimal|number)\((\d+)(,\d+)?\)/i
+ end
+
+ def extract_scale(sql_type)
+ case sql_type
+ when /^(numeric|decimal|number)\((\d+)\)/i then 0
+ when /^(numeric|decimal|number)\((\d+)(,(\d+))\)/i then $4.to_i
+ end
+ end
+
+ def simplified_type(field_type)
+ case field_type
+ when /int/i
+ :integer
+ when /float|double/i
+ :float
+ when /decimal|numeric|number/i
+ extract_scale(field_type) == 0 ? :integer : :decimal
+ when /datetime/i
+ :datetime
+ when /timestamp/i
+ :timestamp
+ when /time/i
+ :time
+ when /date/i
+ :date
+ when /clob/i, /text/i
+ :text
+ when /blob/i, /binary/i
+ :binary
+ when /char/i, /string/i
+ :string
+ when /boolean/i
+ :boolean
+ end
+ end
+ end
+ end
+ # :startdoc:
+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..a04fc01d6f
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
@@ -0,0 +1,657 @@
+# encoding: utf-8
+
+require 'mysql2'
+
+module ActiveRecord
+ class Base
+ def self.mysql2_connection(config)
+ config[:username] = 'root' if config[:username].nil?
+
+ if Mysql2::Client.const_defined? :FOUND_ROWS
+ config[:flags] = Mysql2::Client::FOUND_ROWS
+ end
+
+ 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 Mysql2IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths) #:nodoc:
+ end
+
+ 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 var_name
+ when :text then var_name
+ 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 var_name
+ when :boolean then "#{self.class.name}.value_to_boolean(#{var_name})"
+ else var_name
+ 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 << Mysql2IndexDefinition.new(row[:Table], row[:Key_name], row[:Non_unique] == 0, [], [])
+ end
+
+ indexes.last.columns << row[:Column_name]
+ indexes.last.lengths << row[:Sub_part]
+ 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)
+
+ # By default, MySQL 'where id is null' selects the last inserted id.
+ # Turn this off. http://dev.rubyonrails.org/ticket/6778
+ variable_assignments = ['SQL_AUTO_IS_NULL=0']
+ encoding = @config[:encoding]
+
+ # make sure we set the encoding
+ variable_assignments << "NAMES '#{encoding}'" if encoding
+
+ # increase timeout so mysql server doesn't disconnect us
+ wait_timeout = @config[:wait_timeout]
+ wait_timeout = 2592000 unless wait_timeout.is_a?(Fixnum)
+ variable_assignments << "@@wait_timeout = #{wait_timeout}"
+
+ execute("SET #{variable_assignments.join(', ')}", :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/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
index 47acf0b254..15488cee52 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
@@ -203,6 +203,10 @@ module ActiveRecord
ADAPTER_NAME
end
+ def supports_bulk_alter? #:nodoc:
+ true
+ end
+
# Returns +true+ when the connection adapter supports prepared statement
# caching, otherwise returns +false+
def supports_statement_cache?
@@ -547,11 +551,23 @@ module ActiveRecord
execute "RENAME TABLE #{quote_table_name(table_name)} TO #{quote_table_name(new_name)}"
end
+ def bulk_change_table(table_name, operations) #:nodoc:
+ sqls = operations.map do |command, args|
+ table, arguments = args.shift, args
+ method = :"#{command}_sql"
+
+ if respond_to?(method)
+ send(method, table, *arguments)
+ else
+ raise "Unknown method called : #{method}(#{arguments.inspect})"
+ end
+ end.flatten.join(", ")
+
+ execute("ALTER TABLE #{quote_table_name(table_name)} #{sqls}")
+ 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)
+ execute("ALTER TABLE #{quote_table_name(table_name)} #{add_column_sql(table_name, column_name, type, options)}")
end
def change_column_default(table_name, column_name, default) #:nodoc:
@@ -570,34 +586,11 @@ module ActiveRecord
end
def change_column(table_name, column_name, type, options = {}) #:nodoc:
- 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)
+ execute("ALTER TABLE #{quote_table_name(table_name)} #{change_column_sql(table_name, column_name, type, options)}")
end
def rename_column(table_name, column_name, new_column_name) #:nodoc:
- 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)
+ execute("ALTER TABLE #{quote_table_name(table_name)} #{rename_column_sql(table_name, column_name, new_column_name)}")
end
# Maps logical Rails types to MySQL-specific data types.
@@ -680,6 +673,69 @@ module ActiveRecord
end
end
+ def add_column_sql(table_name, column_name, type, options = {})
+ add_column_sql = "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)
+ add_column_sql
+ end
+
+ def remove_column_sql(table_name, *column_names)
+ columns_for_remove(table_name, *column_names).map {|column_name| "DROP #{column_name}" }
+ end
+ alias :remove_columns_sql :remove_column
+
+ def change_column_sql(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 = "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)
+ change_column_sql
+ end
+
+ def rename_column_sql(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 = "CHANGE #{quote_column_name(column_name)} #{quote_column_name(new_column_name)} #{current_type}"
+ add_column_options!(rename_column_sql, options)
+ rename_column_sql
+ end
+
+ def add_index_sql(table_name, column_name, options = {})
+ index_name, index_type, index_columns = add_index_options(table_name, column_name, options)
+ "ADD #{index_type} INDEX #{index_name} (#{index_columns})"
+ end
+
+ def remove_index_sql(table_name, options = {})
+ index_name = index_name_for_remove(table_name, options)
+ "DROP INDEX #{index_name}"
+ end
+
+ def add_timestamps_sql(table_name)
+ [add_column_sql(table_name, :created_at, :datetime), add_column_sql(table_name, :updated_at, :datetime)]
+ end
+
+ def remove_timestamps_sql(table_name)
+ [remove_column_sql(table_name, :updated_at), remove_column_sql(table_name, :created_at)]
+ end
+
private
def connect
encoding = @config[:encoding]
diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb
index b6f0511b9a..216c691833 100644
--- a/activerecord/lib/active_record/fixtures.rb
+++ b/activerecord/lib/active_record/fixtures.rb
@@ -1,4 +1,10 @@
require 'erb'
+
+begin
+ require 'psych'
+rescue LoadError
+end
+
require 'yaml'
require 'csv'
require 'zlib'
diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb
index d7e481905a..c9d57ce812 100644
--- a/activerecord/lib/active_record/migration/command_recorder.rb
+++ b/activerecord/lib/active_record/migration/command_recorder.rb
@@ -40,7 +40,7 @@ module ActiveRecord
@commands.reverse.map { |name, args|
method = :"invert_#{name}"
raise IrreversibleMigration unless respond_to?(method, true)
- __send__(method, args)
+ send(method, args)
}
end
@@ -48,12 +48,16 @@ module ActiveRecord
super || delegate.respond_to?(*args)
end
- def send(method, *args) # :nodoc:
- return super unless respond_to?(method)
- record(method, args)
+ [:create_table, :rename_table, :add_column, :remove_column, :rename_index, :rename_column, :add_index, :remove_index, :add_timestamps, :remove_timestamps, :change_column, :change_column_default].each do |method|
+ class_eval <<-EOV, __FILE__, __LINE__ + 1
+ def #{method}(*args)
+ record(:"#{method}", args)
+ end
+ EOV
end
private
+
def invert_create_table(args)
[:drop_table, args]
end
@@ -86,6 +90,14 @@ module ActiveRecord
def invert_add_timestamps(args)
[:remove_timestamps, args]
end
+
+ # Forwards any missing method call to the \target.
+ def method_missing(method, *args, &block)
+ @delegate.send(method, *args, &block)
+ rescue NoMethodError => e
+ raise e, e.message.sub(/ for #<.*$/, " via proxy for #{@delegate}")
+ end
+
end
end
end
diff --git a/activerecord/lib/active_record/session_store.rb b/activerecord/lib/active_record/session_store.rb
index 3400fd6ade..e3342f046f 100644
--- a/activerecord/lib/active_record/session_store.rb
+++ b/activerecord/lib/active_record/session_store.rb
@@ -59,10 +59,12 @@ module ActiveRecord
end
def drop_table!
+ reset_column_cache
connection.drop_table table_name
end
def create_table!
+ reset_column_cache
connection.create_table(table_name) do |t|
t.string session_id_column, :limit => 255
t.text data_column_name
diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb
index 5617adea1f..65d9d1fb19 100644
--- a/activerecord/lib/active_record/timestamp.rb
+++ b/activerecord/lib/active_record/timestamp.rb
@@ -66,10 +66,18 @@ module ActiveRecord
self.record_timestamps && (!partial_updates? || changed? || (attributes.keys & self.class.serialized_attributes.keys).present?)
end
+ def timestamp_attributes_for_create_in_model
+ timestamp_attributes_for_create.select { |c| self.class.column_names.include?(c.to_s) }
+ end
+
def timestamp_attributes_for_update_in_model
timestamp_attributes_for_update.select { |c| self.class.column_names.include?(c.to_s) }
end
+ def all_timestamp_attributes_in_model
+ timestamp_attributes_for_create_in_model + timestamp_attributes_for_update_in_model
+ end
+
def timestamp_attributes_for_update #:nodoc:
[:updated_at, :updated_on]
end
diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb
index 48d2f7c9a9..45a4425944 100644
--- a/activerecord/lib/active_record/transactions.rb
+++ b/activerecord/lib/active_record/transactions.rb
@@ -259,7 +259,7 @@ module ActiveRecord
# Call the after_commit callbacks
def committed! #:nodoc:
- _run_commit_callbacks
+ run_callbacks :commit
ensure
clear_transaction_record_state
end
@@ -267,7 +267,7 @@ module ActiveRecord
# Call the after rollback callbacks. The restore_state argument indicates if the record
# state should be rolled back to the beginning or just to the last savepoint.
def rolledback!(force_restore_state = false) #:nodoc:
- _run_rollback_callbacks
+ run_callbacks :rollback
ensure
restore_transaction_record_state(force_restore_state)
end
diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb
index e6a2b40403..a96796f9ff 100644
--- a/activerecord/lib/active_record/validations/uniqueness.rb
+++ b/activerecord/lib/active_record/validations/uniqueness.rb
@@ -15,8 +15,10 @@ module ActiveRecord
def validate_each(record, attribute, value)
finder_class = find_finder_class_for(record)
- if value && record.class.serialized_attributes.key?(attribute.to_s)
- value = YAML.dump value
+ coder = record.class.serialized_attributes[attribute.to_s]
+
+ if value && coder
+ value = coder.dump value
end
sql, params = mount_sql_and_params(finder_class, record.class.quoted_table_name, attribute, value)