aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord/lib')
-rw-r--r--activerecord/lib/active_record.rb2
-rw-r--r--activerecord/lib/active_record/associations/association.rb8
-rw-r--r--activerecord/lib/active_record/associations/association_scope.rb8
-rw-r--r--activerecord/lib/active_record/associations/builder/association.rb2
-rw-r--r--activerecord/lib/active_record/associations/builder/singular_association.rb2
-rw-r--r--activerecord/lib/active_record/associations/collection_association.rb6
-rw-r--r--activerecord/lib/active_record/associations/collection_proxy.rb2
-rw-r--r--activerecord/lib/active_record/associations/has_many_through_association.rb2
-rw-r--r--activerecord/lib/active_record/associations/has_one_association.rb6
-rw-r--r--activerecord/lib/active_record/associations/join_dependency.rb281
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_association.rb40
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_base.rb10
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_part.rb48
-rw-r--r--activerecord/lib/active_record/associations/preloader/through_association.rb21
-rw-r--r--activerecord/lib/active_record/attribute_methods.rb22
-rw-r--r--activerecord/lib/active_record/attribute_methods/serialization.rb7
-rw-r--r--activerecord/lib/active_record/autosave_association.rb398
-rw-r--r--activerecord/lib/active_record/base.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb6
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb8
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb83
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_adapter.rb81
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb6
-rw-r--r--activerecord/lib/active_record/connection_adapters/connection_specification.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql_adapter.rb16
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid.rb12
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb31
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb16
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb64
-rw-r--r--activerecord/lib/active_record/core.rb19
-rw-r--r--activerecord/lib/active_record/enum.rb88
-rw-r--r--activerecord/lib/active_record/fixtures.rb22
-rw-r--r--activerecord/lib/active_record/integration.rb53
-rw-r--r--activerecord/lib/active_record/locking/pessimistic.rb6
-rw-r--r--activerecord/lib/active_record/log_subscriber.rb2
-rw-r--r--activerecord/lib/active_record/migration.rb12
-rw-r--r--activerecord/lib/active_record/model_schema.rb6
-rw-r--r--activerecord/lib/active_record/nested_attributes.rb2
-rw-r--r--activerecord/lib/active_record/no_touching.rb52
-rw-r--r--activerecord/lib/active_record/null_relation.rb2
-rw-r--r--activerecord/lib/active_record/querying.rb2
-rw-r--r--activerecord/lib/active_record/railties/databases.rake2
-rw-r--r--activerecord/lib/active_record/reflection.rb2
-rw-r--r--activerecord/lib/active_record/relation.rb12
-rw-r--r--activerecord/lib/active_record/relation/finder_methods.rb40
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb30
-rw-r--r--activerecord/lib/active_record/result.rb7
-rw-r--r--activerecord/lib/active_record/runtime_registry.rb5
-rw-r--r--activerecord/lib/active_record/schema_migration.rb4
-rw-r--r--activerecord/lib/active_record/store.rb68
-rw-r--r--activerecord/lib/active_record/tasks/database_tasks.rb2
-rw-r--r--activerecord/lib/active_record/tasks/mysql_database_tasks.rb3
-rw-r--r--activerecord/lib/active_record/tasks/postgresql_database_tasks.rb2
-rw-r--r--activerecord/lib/active_record/timestamp.rb4
-rw-r--r--activerecord/lib/active_record/transactions.rb4
56 files changed, 983 insertions, 662 deletions
diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb
index f19f5ecdf9..cbac2ef3c6 100644
--- a/activerecord/lib/active_record.rb
+++ b/activerecord/lib/active_record.rb
@@ -37,6 +37,7 @@ module ActiveRecord
autoload :ConnectionHandling
autoload :CounterCache
autoload :DynamicMatchers
+ autoload :Enum
autoload :Explain
autoload :Inheritance
autoload :Integration
@@ -44,6 +45,7 @@ module ActiveRecord
autoload :Migrator, 'active_record/migration'
autoload :ModelSchema
autoload :NestedAttributes
+ autoload :NoTouching
autoload :Persistence
autoload :QueryCache
autoload :Querying
diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb
index 04c36d5740..e6a45487d0 100644
--- a/activerecord/lib/active_record/associations/association.rb
+++ b/activerecord/lib/active_record/associations/association.rb
@@ -17,6 +17,7 @@ module ActiveRecord
# HasManyThroughAssociation + ThroughAssociation
class Association #:nodoc:
attr_reader :owner, :target, :reflection
+ attr_accessor :inversed
delegate :options, :to => :reflection
@@ -42,6 +43,7 @@ module ActiveRecord
@loaded = false
@target = nil
@stale_state = nil
+ @inversed = false
end
# Reloads the \target and returns +self+ on success.
@@ -59,8 +61,9 @@ module ActiveRecord
# Asserts the \target has been loaded setting the \loaded flag to +true+.
def loaded!
- @loaded = true
+ @loaded = true
@stale_state = stale_state
+ @inversed = false
end
# The target is stale if the target no longer points to the record(s) that the
@@ -70,7 +73,7 @@ module ActiveRecord
#
# Note that if the target has not been loaded, it is not considered stale.
def stale_target?
- loaded? && @stale_state != stale_state
+ !inversed && loaded? && @stale_state != stale_state
end
# Sets the target of this association to <tt>\target</tt>, and the \loaded flag to +true+.
@@ -104,6 +107,7 @@ module ActiveRecord
if record && invertible_for?(record)
inverse = record.association(inverse_reflection_for(record).name)
inverse.target = owner
+ inverse.inversed = true
end
end
diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb
index d862a5f29d..17f056e764 100644
--- a/activerecord/lib/active_record/associations/association_scope.rb
+++ b/activerecord/lib/active_record/associations/association_scope.rb
@@ -78,7 +78,8 @@ module ActiveRecord
scope = scope.joins(join(foreign_table, constraint))
end
- klass = i == 0 ? self.klass : reflection.klass
+ is_first_chain = i == 0
+ klass = is_first_chain ? self.klass : reflection.klass
# Exclude the scope of the association itself, because that
# was already merged in the #scope method.
@@ -89,7 +90,10 @@ module ActiveRecord
scope.merge! item.except(:where, :includes, :bind)
end
- scope.includes! item.includes_values
+ if is_first_chain
+ scope.includes! item.includes_values
+ end
+
scope.where_values += item.where_values
scope.order_values |= item.order_values
end
diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb
index d8d68eb908..37ba1c73b1 100644
--- a/activerecord/lib/active_record/associations/builder/association.rb
+++ b/activerecord/lib/active_record/associations/builder/association.rb
@@ -88,7 +88,7 @@ module ActiveRecord::Associations::Builder
#
# Post.first.comments and Post.first.comments= methods are defined by this method...
def self.define_accessors(model, reflection)
- mixin = model.generated_feature_methods
+ mixin = model.generated_association_methods
name = reflection.name
define_readers(mixin, name)
define_writers(mixin, name)
diff --git a/activerecord/lib/active_record/associations/builder/singular_association.rb b/activerecord/lib/active_record/associations/builder/singular_association.rb
index 2a4b1c441f..66b03c0301 100644
--- a/activerecord/lib/active_record/associations/builder/singular_association.rb
+++ b/activerecord/lib/active_record/associations/builder/singular_association.rb
@@ -8,7 +8,7 @@ module ActiveRecord::Associations::Builder
def self.define_accessors(model, reflection)
super
- define_constructors(model.generated_feature_methods, reflection.name) if reflection.constructable?
+ define_constructors(model.generated_association_methods, reflection.name) if reflection.constructable?
end
# Defines the (build|create)_association methods for belongs_to or has_one association
diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb
index 6b06a5f7fd..62f23f54f9 100644
--- a/activerecord/lib/active_record/associations/collection_association.rb
+++ b/activerecord/lib/active_record/associations/collection_association.rb
@@ -151,7 +151,7 @@ module ActiveRecord
# Removes all records from the association without calling callbacks
# on the associated records. It honors the `:dependent` option. However
- # if the `:dependent` value is `:destroy` then in that case the default
+ # if the `:dependent` value is `:destroy` then in that case the `:delete_all`
# deletion strategy for the association is applied.
#
# You can force a particular deletion strategy by passing a parameter.
@@ -170,9 +170,7 @@ module ActiveRecord
dependent = if dependent.present?
dependent
elsif options[:dependent] == :destroy
- # since delete_all should not invoke callbacks so use the default deletion strategy
- # for :destroy
- reflection.is_a?(ActiveRecord::Reflection::ThroughReflection) ? :delete_all : :nullify
+ :delete_all
else
options[:dependent]
end
diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb
index 0b6cdf5217..2e70a07962 100644
--- a/activerecord/lib/active_record/associations/collection_proxy.rb
+++ b/activerecord/lib/active_record/associations/collection_proxy.rb
@@ -281,7 +281,7 @@ module ActiveRecord
# so method calls may be chained.
#
# class Person < ActiveRecord::Base
- # pets :has_many
+ # has_many :pets
# end
#
# person.pets.size # => 0
diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb
index 56331bbb0b..31b8d27892 100644
--- a/activerecord/lib/active_record/associations/has_many_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_through_association.rb
@@ -163,7 +163,7 @@ module ActiveRecord
delete_through_records(records)
- if source_reflection.options[:counter_cache]
+ if source_reflection.options[:counter_cache] && method != :destroy
counter = source_reflection.counter_cache_column
klass.decrement_counter counter, records.map(&:id)
end
diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb
index 0008600418..944caacab6 100644
--- a/activerecord/lib/active_record/associations/has_one_association.rb
+++ b/activerecord/lib/active_record/associations/has_one_association.rb
@@ -26,11 +26,13 @@ module ActiveRecord
load_target
return self.target if !(target || record)
- if (target != record) || record.changed?
+
+ assigning_another_record = target != record
+ if assigning_another_record || record.changed?
save &&= owner.persisted?
transaction_if(save) do
- remove_target!(options[:dependent]) if target && !target.destroyed?
+ remove_target!(options[:dependent]) if target && !target.destroyed? && assigning_another_record
if record
set_owner_attributes(record)
diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb
index 6e08f67286..c3ac0680ea 100644
--- a/activerecord/lib/active_record/associations/join_dependency.rb
+++ b/activerecord/lib/active_record/associations/join_dependency.rb
@@ -4,6 +4,47 @@ module ActiveRecord
autoload :JoinBase, 'active_record/associations/join_dependency/join_base'
autoload :JoinAssociation, 'active_record/associations/join_dependency/join_association'
+ class Aliases # :nodoc:
+ def initialize(tables)
+ @tables = tables
+ @alias_cache = tables.each_with_object({}) { |table,h|
+ h[table.node] = table.columns.each_with_object({}) { |column,i|
+ i[column.name] = column.alias
+ }
+ }
+ @name_and_alias_cache = tables.each_with_object({}) { |table,h|
+ h[table.node] = table.columns.map { |column|
+ [column.name, column.alias]
+ }
+ }
+ end
+
+ def columns
+ @tables.flat_map { |t| t.column_aliases }
+ end
+
+ # An array of [column_name, alias] pairs for the table
+ def column_aliases(node)
+ @name_and_alias_cache[node]
+ end
+
+ def column_alias(node, column)
+ @alias_cache[node][column]
+ end
+
+ class Table < Struct.new(:node, :columns)
+ def table
+ Arel::Nodes::TableAlias.new node.table, node.aliased_table_name
+ end
+
+ def column_aliases
+ t = table
+ columns.map { |column| t[column.name].as Arel.sql column.alias }
+ end
+ end
+ Column = Struct.new(:name, :alias)
+ end
+
attr_reader :alias_tracker, :base_klass, :join_root
def self.make_tree(associations)
@@ -52,101 +93,117 @@ module ActiveRecord
# joins #=> []
#
def initialize(base, associations, joins)
- @base_klass = base
- @table_joins = joins
- @join_root = JoinBase.new(base)
@alias_tracker = AliasTracker.new(base.connection, joins)
@alias_tracker.aliased_name_for(base.table_name) # Updates the count for base.table_name to 1
tree = self.class.make_tree associations
- build tree, @join_root, Arel::InnerJoin
+ @join_root = JoinBase.new base, build(tree, base)
+ @join_root.children.each { |child| construct_tables! @join_root, child }
end
def reflections
join_root.drop(1).map!(&:reflection)
end
- def merge_outer_joins!(other)
- left = join_root
- right = other.join_root
+ def join_constraints(outer_joins)
+ joins = join_root.children.flat_map { |child|
+ make_inner_joins join_root, child
+ }
- if left.match? right
- merge_node left, right
- else
- # If the roots aren't the same, then deep copy the RHS to the LHS
- left.children.concat right.children.map { |node|
- deep_copy left, node
- }
- end
- end
-
- def join_constraints
- join_root.children.flat_map { |c| c.flat_map(&:join_constraints) }
+ joins.concat outer_joins.flat_map { |oj|
+ if join_root.match? oj.join_root
+ walk join_root, oj.join_root
+ else
+ oj.join_root.children.flat_map { |child|
+ make_outer_joins join_root, child
+ }
+ end
+ }
end
- def columns
- join_root.collect { |join_part|
- table = join_part.aliased_table
- join_part.column_names_with_alias.collect{ |column_name, aliased_name|
- table[column_name].as Arel.sql(aliased_name)
+ def aliases
+ Aliases.new join_root.each_with_index.map { |join_part,i|
+ columns = join_part.column_names.each_with_index.map { |column_name,j|
+ Aliases::Column.new column_name, "t#{i}_r#{j}"
}
- }.flatten
+ Aliases::Table.new(join_part, columns)
+ }
end
- def instantiate(result_set)
- primary_key = join_root.aliased_primary_key
- parents = {}
-
+ def instantiate(result_set, aliases)
+ primary_key = aliases.column_alias(join_root, join_root.primary_key)
type_caster = result_set.column_type primary_key
- records = result_set.map { |row_hash|
+ seen = Hash.new { |h,parent_klass|
+ h[parent_klass] = Hash.new { |i,parent_id|
+ i[parent_id] = Hash.new { |j,child_klass| j[child_klass] = {} }
+ }
+ }
+
+ model_cache = Hash.new { |h,klass| h[klass] = {} }
+ parents = model_cache[join_root]
+ column_aliases = aliases.column_aliases join_root
+
+ result_set.each { |row_hash|
primary_id = type_caster.type_cast row_hash[primary_key]
- parent = parents[primary_id] ||= join_root.instantiate(row_hash)
- construct(parent, join_root, row_hash, result_set)
- parent
- }.uniq
+ parent = parents[primary_id] ||= join_root.instantiate(row_hash, column_aliases)
+ construct(parent, join_root, row_hash, result_set, seen, model_cache, aliases)
+ }
- remove_duplicate_results!(base_klass, records, join_root.children)
- records
+ parents.values
end
private
- def merge_node(left, right)
- intersection, missing = right.children.map { |node1|
- [left.children.find { |node2| node1.match? node2 }, node1]
- }.partition(&:first)
+ def make_constraints(parent, child, tables, join_type)
+ chain = child.reflection.chain
+ foreign_table = parent.table
+ foreign_klass = parent.base_klass
+ child.join_constraints(foreign_table, foreign_klass, child, join_type, tables, child.reflection.scope_chain, chain)
+ end
+
+ def make_outer_joins(parent, child)
+ tables = table_aliases_for(parent, child)
+ join_type = Arel::OuterJoin
+ joins = make_constraints parent, child, tables, join_type
- intersection.each { |l,r| merge_node l, r }
+ joins.concat child.children.flat_map { |c| make_outer_joins(child, c) }
+ end
+
+ def make_inner_joins(parent, child)
+ tables = child.tables
+ join_type = Arel::InnerJoin
+ joins = make_constraints parent, child, tables, join_type
- left.children.concat missing.map { |_,node| deep_copy left, node }
+ joins.concat child.children.flat_map { |c| make_inner_joins(child, c) }
end
- def deep_copy(parent, node)
- dup = build_join_association(node.reflection, parent, Arel::OuterJoin)
- dup.children.concat node.children.map { |n| deep_copy dup, n }
- dup
+ def table_aliases_for(parent, node)
+ node.reflection.chain.map { |reflection|
+ alias_tracker.aliased_table_for(
+ reflection.table_name,
+ table_alias_for(reflection, parent, reflection != node.reflection)
+ )
+ }
end
- def remove_duplicate_results!(base, records, associations)
- associations.each do |node|
- reflection = base.reflect_on_association(node.name)
- remove_uniq_by_reflection(reflection, records)
+ def construct_tables!(parent, node)
+ node.tables = table_aliases_for(parent, node)
+ node.children.each { |child| construct_tables! node, child }
+ end
- parent_records = []
- records.each do |record|
- if descendant = record.send(reflection.name)
- if reflection.collection?
- parent_records.concat descendant.target.uniq
- else
- parent_records << descendant
- end
- end
- end
+ def table_alias_for(reflection, parent, join)
+ name = "#{reflection.plural_name}_#{parent.table_name}"
+ name << "_join" if join
+ name
+ end
- unless parent_records.empty?
- remove_duplicate_results!(reflection.klass, parent_records, node.children)
- end
- end
+ def walk(left, right)
+ intersection, missing = right.children.map { |node1|
+ [left.children.find { |node2| node1.match? node2 }, node1]
+ }.partition(&:first)
+
+ ojs = missing.flat_map { |_,n| make_outer_joins left, n }
+ intersection.flat_map { |l,r| walk l, r }.concat ojs
end
def find_reflection(klass, name)
@@ -154,75 +211,63 @@ module ActiveRecord
raise ConfigurationError, "Association named '#{ name }' was not found on #{ klass.name }; perhaps you misspelled it?"
end
- def build(associations, parent, join_type)
- associations.each do |name, right|
- reflection = find_reflection parent.base_klass, name
- join_association = build_join_association reflection, parent, join_type
- parent.children << join_association
- build right, join_association, join_type
- end
- end
+ def build(associations, base_klass)
+ associations.map do |name, right|
+ reflection = find_reflection base_klass, name
+ reflection.check_validity!
- def build_scalar(reflection, parent, join_type)
- join_association = build_join_association(reflection, parent, join_type)
- parent.children << join_association
- end
+ if reflection.options[:polymorphic]
+ raise EagerLoadPolymorphicError.new(reflection)
+ end
- def remove_uniq_by_reflection(reflection, records)
- if reflection && reflection.collection?
- records.each { |record| record.send(reflection.name).target.uniq! }
+ JoinAssociation.new reflection, build(right, reflection.klass)
end
end
- def build_join_association(reflection, parent, join_type)
- reflection.check_validity!
+ def construct(ar_parent, parent, row, rs, seen, model_cache, aliases)
+ primary_id = ar_parent.id
- if reflection.options[:polymorphic]
- raise EagerLoadPolymorphicError.new(reflection)
- end
-
- JoinAssociation.new(reflection, join_root.to_a.length, parent, join_type, alias_tracker)
- end
-
- def construct(ar_parent, parent, row, rs)
parent.children.each do |node|
- association = construct_association(ar_parent, parent, node, row, rs)
- construct(association, node, row, rs) if association
- end
- end
+ if node.reflection.collection?
+ other = ar_parent.association(node.reflection.name)
+ other.loaded!
+ else
+ if ar_parent.association_cache.key?(node.reflection.name)
+ model = ar_parent.association(node.reflection.name).target
+ construct(model, node, row, rs, seen, model_cache, aliases)
+ next
+ end
+ end
- def construct_association(record, parent, join_part, row, rs)
- caster = rs.column_type(parent.aliased_primary_key)
- row_id = caster.type_cast row[parent.aliased_primary_key]
+ key = aliases.column_alias(node, node.primary_key)
+ id = row[key]
+ next if id.nil?
- return if record.id != row_id
+ model = seen[parent.base_klass][primary_id][node.base_klass][id]
- macro = join_part.reflection.macro
- if macro == :has_one
- return record.association(join_part.reflection.name).target if record.association_cache.key?(join_part.reflection.name)
- association = join_part.instantiate(row) unless row[join_part.aliased_primary_key].nil?
- set_target_and_inverse(join_part, association, record)
- else
- association = join_part.instantiate(row) unless row[join_part.aliased_primary_key].nil?
- case macro
- when :has_many
- other = record.association(join_part.reflection.name)
- other.loaded!
- other.target.push(association) if association
- other.set_inverse_instance(association)
- when :belongs_to
- set_target_and_inverse(join_part, association, record)
+ if model
+ construct(model, node, row, rs, seen, model_cache, aliases)
else
- raise ConfigurationError, "unknown macro: #{join_part.reflection.macro}"
+ model = construct_model(ar_parent, node, row, model_cache, id, aliases)
+ seen[parent.base_klass][primary_id][node.base_klass][id] = model
+ construct(model, node, row, rs, seen, model_cache, aliases)
end
end
- association
end
- def set_target_and_inverse(join_part, association, record)
- other = record.association(join_part.reflection.name)
- other.target = association
- other.set_inverse_instance(association)
+ def construct_model(record, node, row, model_cache, id, aliases)
+ model = model_cache[node][id] ||= node.instantiate(row,
+ aliases.column_aliases(node))
+ other = record.association(node.reflection.name)
+
+ if node.reflection.collection?
+ other.target.push(model)
+ else
+ other.target = model
+ end
+
+ other.set_inverse_instance(model)
+ model
end
end
end
diff --git a/activerecord/lib/active_record/associations/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/join_dependency/join_association.rb
index 3af613d2d1..191d430636 100644
--- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb
+++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb
@@ -4,48 +4,28 @@ module ActiveRecord
module Associations
class JoinDependency # :nodoc:
class JoinAssociation < JoinPart # :nodoc:
- include JoinHelper
-
# The reflection of the association represented
attr_reader :reflection
- # What type of join will be generated, either Arel::InnerJoin (default) or Arel::OuterJoin
- attr_accessor :join_type
-
- # These implement abstract methods from the superclass
- attr_reader :aliased_prefix
-
- attr_reader :tables
- attr_reader :alias_tracker
+ attr_accessor :tables
- delegate :options, :through_reflection, :source_reflection, :chain, :to => :reflection
-
- def initialize(reflection, index, parent, join_type, alias_tracker)
- super(reflection.klass, parent)
+ def initialize(reflection, children)
+ super(reflection.klass, children)
@reflection = reflection
- @alias_tracker = alias_tracker
- @join_type = join_type
- @aliased_prefix = "t#{ index }"
- @tables = construct_tables.reverse
+ @tables = nil
end
- def parent_table_name; parent.table_name; end
- alias :alias_suffix :parent_table_name
-
def match?(other)
return true if self == other
super && reflection == other.reflection
end
- def join_constraints
+ def join_constraints(foreign_table, foreign_klass, node, join_type, tables, scope_chain, chain)
joins = []
- tables = @tables.dup
-
- foreign_table = parent.table
- foreign_klass = parent.base_klass
+ tables = tables.reverse
- scope_chain_iter = reflection.scope_chain.reverse_each
+ scope_chain_iter = scope_chain.reverse_each
# The chain starts with the target table, but we want to end with it here (makes
# more sense in this context), so we reverse
@@ -68,7 +48,7 @@ module ActiveRecord
if item.is_a?(Relation)
item
else
- ActiveRecord::Relation.create(klass, table).instance_exec(self, &item)
+ ActiveRecord::Relation.create(klass, table).instance_exec(node, &item)
end
end
@@ -88,7 +68,7 @@ module ActiveRecord
constraint = constraint.and rel.arel.constraints
end
- joins << join(table, constraint)
+ joins << table.create_join(table, table.create_on(constraint), join_type)
# The current table in this iteration becomes the foreign table in the next
foreign_table, foreign_klass = table, klass
@@ -126,7 +106,7 @@ module ActiveRecord
end
def table
- tables.last
+ tables.first
end
def aliased_table_name
diff --git a/activerecord/lib/active_record/associations/join_dependency/join_base.rb b/activerecord/lib/active_record/associations/join_dependency/join_base.rb
index 48de12bcd5..3a26c25737 100644
--- a/activerecord/lib/active_record/associations/join_dependency/join_base.rb
+++ b/activerecord/lib/active_record/associations/join_dependency/join_base.rb
@@ -4,21 +4,13 @@ module ActiveRecord
module Associations
class JoinDependency # :nodoc:
class JoinBase < JoinPart # :nodoc:
- def initialize(klass)
- super(klass, nil)
- end
-
def match?(other)
return true if self == other
super && base_klass == other.base_klass
end
- def aliased_prefix
- "t0"
- end
-
def table
- Arel::Table.new(table_name, arel_engine)
+ base_klass.arel_table
end
def aliased_table_name
diff --git a/activerecord/lib/active_record/associations/join_dependency/join_part.rb b/activerecord/lib/active_record/associations/join_dependency/join_part.rb
index d39ce94c99..91e1c6a9d7 100644
--- a/activerecord/lib/active_record/associations/join_dependency/join_part.rb
+++ b/activerecord/lib/active_record/associations/join_dependency/join_part.rb
@@ -10,23 +10,17 @@ module ActiveRecord
class JoinPart # :nodoc:
include Enumerable
- # A JoinBase instance representing the active record we are joining onto.
- # (So in Author.has_many :posts, the Author would be that base record.)
- attr_reader :parent
-
# The Active Record class which this join part is associated 'about'; for a JoinBase
# this is the actual base model, for a JoinAssociation this is the target model of the
# association.
attr_reader :base_klass, :children
- delegate :table_name, :column_names, :primary_key, :arel_engine, :to => :base_klass
+ delegate :table_name, :column_names, :primary_key, :to => :base_klass
- def initialize(base_klass, parent)
+ def initialize(base_klass, children)
@base_klass = base_klass
- @parent = parent
- @cached_record = {}
@column_names_with_alias = nil
- @children = []
+ @children = children
end
def name
@@ -42,43 +36,17 @@ module ActiveRecord
children.each { |child| child.each(&block) }
end
- def aliased_table
- Arel::Nodes::TableAlias.new table, aliased_table_name
- end
-
# An Arel::Table for the active_record
def table
raise NotImplementedError
end
- # The prefix to be used when aliasing columns in the active_record's table
- def aliased_prefix
- raise NotImplementedError
- end
-
# The alias for the active_record's table
def aliased_table_name
raise NotImplementedError
end
- # The alias for the primary key of the active_record's table
- def aliased_primary_key
- "#{aliased_prefix}_r0"
- end
-
- # An array of [column_name, alias] pairs for the table
- def column_names_with_alias
- unless @column_names_with_alias
- @column_names_with_alias = []
-
- column_names.each_with_index do |column_name, i|
- @column_names_with_alias << [column_name, "#{aliased_prefix}_r#{i}"]
- end
- end
- @column_names_with_alias
- end
-
- def extract_record(row)
+ def extract_record(row, column_names_with_alias)
# This code is performance critical as it is called per row.
# see: https://github.com/rails/rails/pull/12185
hash = {}
@@ -95,12 +63,8 @@ module ActiveRecord
hash
end
- def record_id(row)
- row[aliased_primary_key]
- end
-
- def instantiate(row)
- @cached_record[record_id(row)] ||= base_klass.instantiate(extract_record(row))
+ def instantiate(row, aliases)
+ base_klass.instantiate(extract_record(row, aliases))
end
end
end
diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb
index 3166df57eb..2a8530af62 100644
--- a/activerecord/lib/active_record/associations/preloader/through_association.rb
+++ b/activerecord/lib/active_record/associations/preloader/through_association.rb
@@ -29,29 +29,30 @@ module ActiveRecord
source_reflection.name,
reflection_scope)
+ @preloaded_records = preloaders.flat_map(&:preloaded_records)
+
middle_to_pl = preloaders.each_with_object({}) do |pl,h|
pl.owners.each { |middle|
h[middle] = pl
}
end
+ record_offset = {}
+ @preloaded_records.each_with_index do |record,i|
+ record_offset[record] = i
+ end
+
through_records.each_with_object({}) { |(lhs,center),records_by_owner|
pl_to_middle = center.group_by { |record| middle_to_pl[record] }
records_by_owner[lhs] = pl_to_middle.flat_map do |pl, middles|
rhs_records = middles.flat_map { |r|
- r.send(source_reflection.name)
+ association = r.association source_reflection.name
+
+ association.reader
}.compact
- loaded_records = pl.preloaded_records
- i = 0
- record_index = loaded_records.each_with_object({}) { |r,indexes|
- indexes[r] = i
- i += 1
- }
- records = rhs_records.sort_by { |rhs| record_index[rhs] }
- @preloaded_records.concat rhs_records
- records
+ rhs_records.sort_by { |rhs| record_offset[rhs] }
end
}
end
diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb
index bf270c1829..3924eec872 100644
--- a/activerecord/lib/active_record/attribute_methods.rb
+++ b/activerecord/lib/active_record/attribute_methods.rb
@@ -161,12 +161,9 @@ module ActiveRecord
# If we haven't generated any methods yet, generate them, then
# see if we've created the method we're looking for.
def method_missing(method, *args, &block) # :nodoc:
- if self.class.define_attribute_methods
- if respond_to_without_attributes?(method)
- send(method, *args, &block)
- else
- super
- end
+ self.class.define_attribute_methods
+ if respond_to_without_attributes?(method)
+ send(method, *args, &block)
else
super
end
@@ -248,9 +245,10 @@ module ActiveRecord
# Returns an <tt>#inspect</tt>-like string for the value of the
# attribute +attr_name+. String attributes are truncated upto 50
- # characters, and Date and Time attributes are returned in the
- # <tt>:db</tt> format. Other attributes return the value of
- # <tt>#inspect</tt> without modification.
+ # characters, Date and Time attributes are returned in the
+ # <tt>:db</tt> format, Array attributes are truncated upto 10 values.
+ # Other attributes return the value of <tt>#inspect</tt> without
+ # modification.
#
# person = Person.create!(name: 'David Heinemeier Hansson ' * 3)
#
@@ -259,6 +257,9 @@ module ActiveRecord
#
# person.attribute_for_inspect(:created_at)
# # => "\"2012-10-22 00:15:07\""
+ #
+ # person.attribute_for_inspect(:tag_ids)
+ # # => "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]"
def attribute_for_inspect(attr_name)
value = read_attribute(attr_name)
@@ -266,6 +267,9 @@ module ActiveRecord
"#{value[0, 50]}...".inspect
elsif value.is_a?(Date) || value.is_a?(Time)
%("#{value.to_s(:db)}")
+ elsif value.is_a?(Array) && value.size > 10
+ inspected = value.first(10).inspect
+ %(#{inspected[0...-1]}, ...])
else
value.inspect
end
diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb
index 1287de0d0d..d484659190 100644
--- a/activerecord/lib/active_record/attribute_methods/serialization.rb
+++ b/activerecord/lib/active_record/attribute_methods/serialization.rb
@@ -24,6 +24,9 @@ module ActiveRecord
# serialized object must be of that class on retrieval or
# <tt>SerializationTypeMismatch</tt> will be raised.
#
+ # A notable side effect of serialized attributes is that the model will
+ # be updated on every save, even if it is not dirty.
+ #
# ==== Parameters
#
# * +attr_name+ - The field name that should be serialized.
@@ -66,6 +69,10 @@ module ActiveRecord
def type
@column.type
end
+
+ def accessor
+ ActiveRecord::Store::IndifferentHashAccessor
+ end
end
class Attribute < Struct.new(:coder, :value, :state) # :nodoc:
diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb
index b30d1eb0a6..e9622ca0c1 100644
--- a/activerecord/lib/active_record/autosave_association.rb
+++ b/activerecord/lib/active_record/autosave_association.rb
@@ -143,71 +143,71 @@ module ActiveRecord
module ClassMethods
private
- def define_non_cyclic_method(name, reflection, &block)
- define_method(name) do |*args|
- result = true; @_already_called ||= {}
- # Loop prevention for validation of associations
- unless @_already_called[[name, reflection.name]]
- begin
- @_already_called[[name, reflection.name]]=true
- result = instance_eval(&block)
- ensure
- @_already_called[[name, reflection.name]]=false
+ def define_non_cyclic_method(name, &block)
+ define_method(name) do |*args|
+ result = true; @_already_called ||= {}
+ # Loop prevention for validation of associations
+ unless @_already_called[name]
+ begin
+ @_already_called[name]=true
+ result = instance_eval(&block)
+ ensure
+ @_already_called[name]=false
+ end
end
- end
- result
+ result
+ end
end
- end
- # Adds validation and save callbacks for the association as specified by
- # the +reflection+.
- #
- # For performance reasons, we don't check whether to validate at runtime.
- # However the validation and callback methods are lazy and those methods
- # get created when they are invoked for the very first time. However,
- # this can change, for instance, when using nested attributes, which is
- # called _after_ the association has been defined. Since we don't want
- # the callbacks to get defined multiple times, there are guards that
- # check if the save or validation methods have already been defined
- # before actually defining them.
- def add_autosave_association_callbacks(reflection)
- save_method = :"autosave_associated_records_for_#{reflection.name}"
- validation_method = :"validate_associated_records_for_#{reflection.name}"
- collection = reflection.collection?
-
- unless method_defined?(save_method)
- if collection
- before_save :before_save_collection_association
-
- define_non_cyclic_method(save_method, reflection) { save_collection_association(reflection) }
- # Doesn't use after_save as that would save associations added in after_create/after_update twice
- after_create save_method
- after_update save_method
- elsif reflection.macro == :has_one
- define_method(save_method) { save_has_one_association(reflection) }
- # Configures two callbacks instead of a single after_save so that
- # the model may rely on their execution order relative to its
- # own callbacks.
- #
- # For example, given that after_creates run before after_saves, if
- # we configured instead an after_save there would be no way to fire
- # a custom after_create callback after the child association gets
- # created.
- after_create save_method
- after_update save_method
- else
- define_non_cyclic_method(save_method, reflection) { save_belongs_to_association(reflection) }
- before_save save_method
+ # Adds validation and save callbacks for the association as specified by
+ # the +reflection+.
+ #
+ # For performance reasons, we don't check whether to validate at runtime.
+ # However the validation and callback methods are lazy and those methods
+ # get created when they are invoked for the very first time. However,
+ # this can change, for instance, when using nested attributes, which is
+ # called _after_ the association has been defined. Since we don't want
+ # the callbacks to get defined multiple times, there are guards that
+ # check if the save or validation methods have already been defined
+ # before actually defining them.
+ def add_autosave_association_callbacks(reflection)
+ save_method = :"autosave_associated_records_for_#{reflection.name}"
+ validation_method = :"validate_associated_records_for_#{reflection.name}"
+ collection = reflection.collection?
+
+ unless method_defined?(save_method)
+ if collection
+ before_save :before_save_collection_association
+
+ define_non_cyclic_method(save_method) { save_collection_association(reflection) }
+ # Doesn't use after_save as that would save associations added in after_create/after_update twice
+ after_create save_method
+ after_update save_method
+ elsif reflection.macro == :has_one
+ define_method(save_method) { save_has_one_association(reflection) }
+ # Configures two callbacks instead of a single after_save so that
+ # the model may rely on their execution order relative to its
+ # own callbacks.
+ #
+ # For example, given that after_creates run before after_saves, if
+ # we configured instead an after_save there would be no way to fire
+ # a custom after_create callback after the child association gets
+ # created.
+ after_create save_method
+ after_update save_method
+ else
+ define_non_cyclic_method(save_method) { save_belongs_to_association(reflection) }
+ before_save save_method
+ end
end
- end
- if reflection.validate? && !method_defined?(validation_method)
- method = (collection ? :validate_collection_association : :validate_single_association)
- define_non_cyclic_method(validation_method, reflection) { send(method, reflection) }
- validate validation_method
+ if reflection.validate? && !method_defined?(validation_method)
+ method = (collection ? :validate_collection_association : :validate_single_association)
+ define_non_cyclic_method(validation_method) { send(method, reflection) }
+ validate validation_method
+ end
end
- end
end
# Reloads the attributes of the object as usual and clears <tt>marked_for_destruction</tt> flag.
@@ -254,173 +254,179 @@ module ActiveRecord
private
- # Returns the record for an association collection that should be validated
- # or saved. If +autosave+ is +false+ only new records will be returned,
- # unless the parent is/was a new record itself.
- def associated_records_to_validate_or_save(association, new_record, autosave)
- if new_record
- association && association.target
- elsif autosave
- association.target.find_all { |record| record.changed_for_autosave? }
- else
- association.target.find_all { |record| record.new_record? }
+ # Returns the record for an association collection that should be validated
+ # or saved. If +autosave+ is +false+ only new records will be returned,
+ # unless the parent is/was a new record itself.
+ def associated_records_to_validate_or_save(association, new_record, autosave)
+ if new_record
+ association && association.target
+ elsif autosave
+ association.target.find_all { |record| record.changed_for_autosave? }
+ else
+ association.target.find_all { |record| record.new_record? }
+ end
end
- end
- # go through nested autosave associations that are loaded in memory (without loading
- # any new ones), and return true if is changed for autosave
- def nested_records_changed_for_autosave?
- self.class.reflect_on_all_autosave_associations.any? do |reflection|
- association = association_instance_get(reflection.name)
- association && Array.wrap(association.target).any? { |a| a.changed_for_autosave? }
+ # go through nested autosave associations that are loaded in memory (without loading
+ # any new ones), and return true if is changed for autosave
+ def nested_records_changed_for_autosave?
+ self.class.reflect_on_all_autosave_associations.any? do |reflection|
+ association = association_instance_get(reflection.name)
+ association && Array.wrap(association.target).any? { |a| a.changed_for_autosave? }
+ end
end
- end
- # Validate the association if <tt>:validate</tt> or <tt>:autosave</tt> is
- # turned on for the association.
- def validate_single_association(reflection)
- association = association_instance_get(reflection.name)
- record = association && association.reader
- association_valid?(reflection, record) if record
- end
+ # Validate the association if <tt>:validate</tt> or <tt>:autosave</tt> is
+ # turned on for the association.
+ def validate_single_association(reflection)
+ association = association_instance_get(reflection.name)
+ record = association && association.reader
+ association_valid?(reflection, record) if record
+ end
- # Validate the associated records if <tt>:validate</tt> or
- # <tt>:autosave</tt> is turned on for the association specified by
- # +reflection+.
- def validate_collection_association(reflection)
- if association = association_instance_get(reflection.name)
- if records = associated_records_to_validate_or_save(association, new_record?, reflection.options[:autosave])
- records.each { |record| association_valid?(reflection, record) }
+ # Validate the associated records if <tt>:validate</tt> or
+ # <tt>:autosave</tt> is turned on for the association specified by
+ # +reflection+.
+ def validate_collection_association(reflection)
+ if association = association_instance_get(reflection.name)
+ if records = associated_records_to_validate_or_save(association, new_record?, reflection.options[:autosave])
+ records.each { |record| association_valid?(reflection, record) }
+ end
end
end
- end
- # Returns whether or not the association is valid and applies any errors to
- # the parent, <tt>self</tt>, if it wasn't. Skips any <tt>:autosave</tt>
- # enabled records if they're marked_for_destruction? or destroyed.
- def association_valid?(reflection, record)
- return true if record.destroyed? || record.marked_for_destruction?
-
- unless valid = record.valid?
- if reflection.options[:autosave]
- record.errors.each do |attribute, message|
- attribute = "#{reflection.name}.#{attribute}"
- errors[attribute] << message
- errors[attribute].uniq!
+ # Returns whether or not the association is valid and applies any errors to
+ # the parent, <tt>self</tt>, if it wasn't. Skips any <tt>:autosave</tt>
+ # enabled records if they're marked_for_destruction? or destroyed.
+ def association_valid?(reflection, record)
+ return true if record.destroyed? || record.marked_for_destruction?
+
+ unless valid = record.valid?
+ if reflection.options[:autosave]
+ record.errors.each do |attribute, message|
+ attribute = "#{reflection.name}.#{attribute}"
+ errors[attribute] << message
+ errors[attribute].uniq!
+ end
+ else
+ errors.add(reflection.name)
end
- else
- errors.add(reflection.name)
end
+ valid
end
- valid
- end
- # Is used as a before_save callback to check while saving a collection
- # association whether or not the parent was a new record before saving.
- def before_save_collection_association
- @new_record_before_save = new_record?
- true
- end
+ # Is used as a before_save callback to check while saving a collection
+ # association whether or not the parent was a new record before saving.
+ def before_save_collection_association
+ @new_record_before_save = new_record?
+ true
+ end
- # Saves any new associated records, or all loaded autosave associations if
- # <tt>:autosave</tt> is enabled on the association.
- #
- # In addition, it destroys all children that were marked for destruction
- # with mark_for_destruction.
- #
- # This all happens inside a transaction, _if_ the Transactions module is included into
- # ActiveRecord::Base after the AutosaveAssociation module, which it does by default.
- def save_collection_association(reflection)
- if association = association_instance_get(reflection.name)
- autosave = reflection.options[:autosave]
-
- if records = associated_records_to_validate_or_save(association, @new_record_before_save, autosave)
-
- if autosave
- records_to_destroy = records.select(&:marked_for_destruction?)
- records_to_destroy.each { |record| association.destroy(record) }
- records -= records_to_destroy
- end
+ # Saves any new associated records, or all loaded autosave associations if
+ # <tt>:autosave</tt> is enabled on the association.
+ #
+ # In addition, it destroys all children that were marked for destruction
+ # with mark_for_destruction.
+ #
+ # This all happens inside a transaction, _if_ the Transactions module is included into
+ # ActiveRecord::Base after the AutosaveAssociation module, which it does by default.
+ def save_collection_association(reflection)
+ if association = association_instance_get(reflection.name)
+ autosave = reflection.options[:autosave]
+
+ if records = associated_records_to_validate_or_save(association, @new_record_before_save, autosave)
+
+ if autosave
+ records_to_destroy = records.select(&:marked_for_destruction?)
+ records_to_destroy.each { |record| association.destroy(record) }
+ records -= records_to_destroy
+ end
- records.each do |record|
- next if record.destroyed?
+ records.each do |record|
+ next if record.destroyed?
- saved = true
+ saved = true
- if autosave != false && (@new_record_before_save || record.new_record?)
- if autosave
- saved = association.insert_record(record, false)
- else
- association.insert_record(record) unless reflection.nested?
+ if autosave != false && (@new_record_before_save || record.new_record?)
+ if autosave
+ saved = association.insert_record(record, false)
+ else
+ association.insert_record(record) unless reflection.nested?
+ end
+ elsif autosave
+ saved = record.save(:validate => false)
end
- elsif autosave
- saved = record.save(:validate => false)
- end
- raise ActiveRecord::Rollback unless saved
+ raise ActiveRecord::Rollback unless saved
+ end
end
- end
- # reconstruct the scope now that we know the owner's id
- association.reset_scope if association.respond_to?(:reset_scope)
+ # reconstruct the scope now that we know the owner's id
+ association.reset_scope if association.respond_to?(:reset_scope)
+ end
end
- end
- # Saves the associated record if it's new or <tt>:autosave</tt> is enabled
- # on the association.
- #
- # In addition, it will destroy the association if it was marked for
- # destruction with mark_for_destruction.
- #
- # This all happens inside a transaction, _if_ the Transactions module is included into
- # ActiveRecord::Base after the AutosaveAssociation module, which it does by default.
- def save_has_one_association(reflection)
- association = association_instance_get(reflection.name)
- record = association && association.load_target
- if record && !record.destroyed?
- autosave = reflection.options[:autosave]
-
- if autosave && record.marked_for_destruction?
- record.destroy
- else
- key = reflection.options[:primary_key] ? send(reflection.options[:primary_key]) : id
- if autosave != false && (new_record? || record.new_record? || record[reflection.foreign_key] != key || autosave)
- unless reflection.through_reflection
- record[reflection.foreign_key] = key
- end
+ # Saves the associated record if it's new or <tt>:autosave</tt> is enabled
+ # on the association.
+ #
+ # In addition, it will destroy the association if it was marked for
+ # destruction with mark_for_destruction.
+ #
+ # This all happens inside a transaction, _if_ the Transactions module is included into
+ # ActiveRecord::Base after the AutosaveAssociation module, which it does by default.
+ def save_has_one_association(reflection)
+ association = association_instance_get(reflection.name)
+ record = association && association.load_target
+ if record && !record.destroyed?
+ autosave = reflection.options[:autosave]
- saved = record.save(:validate => !autosave)
- raise ActiveRecord::Rollback if !saved && autosave
- saved
+ if autosave && record.marked_for_destruction?
+ record.destroy
+ else
+ key = reflection.options[:primary_key] ? send(reflection.options[:primary_key]) : id
+ if autosave != false && (autosave || new_record? || record_changed?(reflection, record, key))
+
+ unless reflection.through_reflection
+ record[reflection.foreign_key] = key
+ end
+
+ saved = record.save(:validate => !autosave)
+ raise ActiveRecord::Rollback if !saved && autosave
+ saved
+ end
end
end
end
- end
- # Saves the associated record if it's new or <tt>:autosave</tt> is enabled.
- #
- # In addition, it will destroy the association if it was marked for destruction.
- def save_belongs_to_association(reflection)
- association = association_instance_get(reflection.name)
- record = association && association.load_target
- if record && !record.destroyed?
- autosave = reflection.options[:autosave]
-
- if autosave && record.marked_for_destruction?
- self[reflection.foreign_key] = nil
- record.destroy
- elsif autosave != false
- saved = record.save(:validate => !autosave) if record.new_record? || (autosave && record.changed_for_autosave?)
-
- if association.updated?
- association_id = record.send(reflection.options[:primary_key] || :id)
- self[reflection.foreign_key] = association_id
- association.loaded!
- end
+ # If the record is new or it has changed, returns true.
+ def record_changed?(reflection, record, key)
+ record.new_record? || record[reflection.foreign_key] != key || record.attribute_changed?(reflection.foreign_key)
+ end
- saved if autosave
+ # Saves the associated record if it's new or <tt>:autosave</tt> is enabled.
+ #
+ # In addition, it will destroy the association if it was marked for destruction.
+ def save_belongs_to_association(reflection)
+ association = association_instance_get(reflection.name)
+ record = association && association.load_target
+ if record && !record.destroyed?
+ autosave = reflection.options[:autosave]
+
+ if autosave && record.marked_for_destruction?
+ self[reflection.foreign_key] = nil
+ record.destroy
+ elsif autosave != false
+ saved = record.save(:validate => !autosave) if record.new_record? || (autosave && record.changed_for_autosave?)
+
+ if association.updated?
+ association_id = record.send(reflection.options[:primary_key] || :id)
+ self[reflection.foreign_key] = association_id
+ association.loaded!
+ end
+
+ saved if autosave
+ end
end
end
- end
end
end
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index 04e3dd49e7..e05e22ebb0 100644
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -291,9 +291,11 @@ module ActiveRecord #:nodoc:
extend Translation
extend DynamicMatchers
extend Explain
+ extend Enum
extend Delegation::DelegateCache
include Persistence
+ include NoTouching
include ReadonlyAttributes
include ModelSchema
include Inheritance
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
index e1f29ea03a..06a2ddb8b7 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
@@ -286,6 +286,10 @@ module ActiveRecord
# Inserts the given fixture into the table. Overridden in adapters that require
# something beyond a simple insert (eg. Oracle).
def insert_fixture(fixture, table_name)
+ execute fixture_sql(fixture, table_name), 'Fixture Insert'
+ end
+
+ def fixture_sql(fixture, table_name)
columns = schema_cache.columns_hash(table_name)
key_list = []
@@ -294,7 +298,7 @@ module ActiveRecord
quote(value, columns[name])
end
- execute "INSERT INTO #{quote_table_name(table_name)} (#{key_list.join(', ')}) VALUES (#{value_list.join(', ')})", 'Fixture Insert'
+ "INSERT INTO #{quote_table_name(table_name)} (#{key_list.join(', ')}) VALUES (#{value_list.join(', ')})"
end
def empty_insert_statement_value
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
index 7aae297cdc..8399232d73 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
@@ -9,10 +9,10 @@ module ActiveRecord
def dirties_query_cache(base, *method_names)
method_names.each do |method_name|
base.class_eval <<-end_code, __FILE__, __LINE__ + 1
- def #{method_name}(*) # def update_with_query_dirty(*)
- clear_query_cache if @query_cache_enabled # clear_query_cache if @query_cache_enabled
- super # super
- end # end
+ def #{method_name}(*)
+ clear_query_cache if @query_cache_enabled
+ super
+ end
end_code
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
new file mode 100644
index 0000000000..7c330a2f25
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
@@ -0,0 +1,83 @@
+module ActiveRecord
+ module ConnectionAdapters
+ class AbstractAdapter
+ class SchemaCreation # :nodoc:
+ def initialize(conn)
+ @conn = conn
+ @cache = {}
+ end
+
+ def accept(o)
+ m = @cache[o.class] ||= "visit_#{o.class.name.split('::').last}"
+ send m, o
+ end
+
+ def visit_AddColumn(o)
+ sql_type = type_to_sql(o.type.to_sym, o.limit, o.precision, o.scale)
+ sql = "ADD #{quote_column_name(o.name)} #{sql_type}"
+ add_column_options!(sql, column_options(o))
+ end
+
+ private
+
+ def visit_AlterTable(o)
+ sql = "ALTER TABLE #{quote_table_name(o.name)} "
+ sql << o.adds.map { |col| visit_AddColumn col }.join(' ')
+ end
+
+ def visit_ColumnDefinition(o)
+ sql_type = type_to_sql(o.type.to_sym, o.limit, o.precision, o.scale)
+ column_sql = "#{quote_column_name(o.name)} #{sql_type}"
+ add_column_options!(column_sql, column_options(o)) unless o.primary_key?
+ column_sql
+ end
+
+ def visit_TableDefinition(o)
+ create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE "
+ create_sql << "#{quote_table_name(o.name)} ("
+ create_sql << o.columns.map { |c| accept c }.join(', ')
+ create_sql << ") #{o.options}"
+ create_sql
+ end
+
+ def column_options(o)
+ column_options = {}
+ column_options[:null] = o.null unless o.null.nil?
+ column_options[:default] = o.default unless o.default.nil?
+ column_options[:column] = o
+ column_options[:first] = o.first
+ column_options[:after] = o.after
+ column_options
+ end
+
+ def quote_column_name(name)
+ @conn.quote_column_name name
+ end
+
+ def quote_table_name(name)
+ @conn.quote_table_name name
+ end
+
+ def type_to_sql(type, limit, precision, scale)
+ @conn.type_to_sql type.to_sym, limit, precision, scale
+ end
+
+ def add_column_options!(sql, options)
+ sql << " DEFAULT #{@conn.quote(options[:default], options[:column])}" if options_include_default?(options)
+ # must explicitly check for :null to allow change_column to work on migrations
+ if options[:null] == false
+ sql << " NOT NULL"
+ end
+ if options[:auto_increment] == true
+ sql << " AUTO_INCREMENT"
+ end
+ sql
+ end
+
+ def options_include_default?(options)
+ options.include?(:default) && !(options[:null] == false && options[:default].nil?)
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
index cbe563676b..8aa1ce5c04 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -4,6 +4,7 @@ require 'bigdecimal/util'
require 'active_support/core_ext/benchmark'
require 'active_record/connection_adapters/schema_cache'
require 'active_record/connection_adapters/abstract/schema_dumper'
+require 'active_record/connection_adapters/abstract/schema_creation'
require 'monitor'
module ActiveRecord
@@ -16,6 +17,7 @@ module ActiveRecord
autoload_at 'active_record/connection_adapters/abstract/schema_definitions' do
autoload :IndexDefinition
autoload :ColumnDefinition
+ autoload :ChangeColumnDefinition
autoload :TableDefinition
autoload :Table
autoload :AlterTable
@@ -40,6 +42,7 @@ module ActiveRecord
autoload :ClosedTransaction
autoload :RealTransaction
autoload :SavepointTransaction
+ autoload :TransactionState
end
# Active Record supports multiple database systems. AbstractAdapter and
@@ -105,84 +108,6 @@ module ActiveRecord
true
end
- class SchemaCreation
- def initialize(conn)
- @conn = conn
- @cache = {}
- end
-
- def accept(o)
- m = @cache[o.class] ||= "visit_#{o.class.name.split('::').last}"
- send m, o
- end
-
- def visit_AddColumn(o)
- sql_type = type_to_sql(o.type.to_sym, o.limit, o.precision, o.scale)
- sql = "ADD #{quote_column_name(o.name)} #{sql_type}"
- add_column_options!(sql, column_options(o))
- end
-
- private
-
- def visit_AlterTable(o)
- sql = "ALTER TABLE #{quote_table_name(o.name)} "
- sql << o.adds.map { |col| visit_AddColumn col }.join(' ')
- end
-
- def visit_ColumnDefinition(o)
- sql_type = type_to_sql(o.type.to_sym, o.limit, o.precision, o.scale)
- column_sql = "#{quote_column_name(o.name)} #{sql_type}"
- add_column_options!(column_sql, column_options(o)) unless o.primary_key?
- column_sql
- end
-
- def visit_TableDefinition(o)
- create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE "
- create_sql << "#{quote_table_name(o.name)} ("
- create_sql << o.columns.map { |c| accept c }.join(', ')
- create_sql << ") #{o.options}"
- create_sql
- end
-
- def column_options(o)
- column_options = {}
- column_options[:null] = o.null unless o.null.nil?
- column_options[:default] = o.default unless o.default.nil?
- column_options[:column] = o
- column_options[:first] = o.first
- column_options[:after] = o.after
- column_options
- end
-
- def quote_column_name(name)
- @conn.quote_column_name name
- end
-
- def quote_table_name(name)
- @conn.quote_table_name name
- end
-
- def type_to_sql(type, limit, precision, scale)
- @conn.type_to_sql type.to_sym, limit, precision, scale
- end
-
- def add_column_options!(sql, options)
- sql << " DEFAULT #{@conn.quote(options[:default], options[:column])}" if options_include_default?(options)
- # must explicitly check for :null to allow change_column to work on migrations
- if options[:null] == false
- sql << " NOT NULL"
- end
- if options[:auto_increment] == true
- sql << " AUTO_INCREMENT"
- end
- sql
- end
-
- def options_include_default?(options)
- options.include?(:default) && !(options[:null] == false && options[:default].nil?)
- end
- end
-
def schema_creation
SchemaCreation.new self
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
index 138ab811dc..ee6ca4fb6f 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
@@ -206,6 +206,12 @@ module ActiveRecord
true
end
+ def type_cast(value, column)
+ return super unless value == true || value == false
+
+ value ? 1 : 0
+ end
+
# MySQL 4 technically support transaction isolation, but it is affected by a bug
# where the transaction level gets persisted for the whole session:
#
diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb
index 8bad7d0cf5..64fc9e95d8 100644
--- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb
+++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb
@@ -55,7 +55,7 @@ module ActiveRecord
begin
require path_to_adapter
rescue Gem::LoadError => e
- raise Gem::LoadError, "Specified '#{spec[:adapter]}' for database adapter, but the gem is not loaded. Add `gem '#{e.name}'` to your Gemfile."
+ raise Gem::LoadError, "Specified '#{spec[:adapter]}' for database adapter, but the gem is not loaded. Add `gem '#{e.name}'` to your Gemfile (and ensure its version is at the minimum required by ActiveRecord)."
rescue LoadError => e
raise LoadError, "Could not load '#{path_to_adapter}'. Make sure that the adapter in config/database.yml is valid. If you use an adapter other than 'mysql', 'mysql2', 'postgresql' or 'sqlite3' add the necessary adapter gem to the Gemfile.", e.backtrace
end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
index 41a47183e0..7b18b95c44 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
@@ -160,12 +160,6 @@ module ActiveRecord
# QUOTING ==================================================
- def type_cast(value, column)
- return super unless value == true || value == false
-
- value ? 1 : 0
- end
-
def quote_string(string) #:nodoc:
@connection.quote(string)
end
@@ -476,7 +470,11 @@ module ActiveRecord
def exec_stmt(sql, name, binds)
cache = {}
- log(sql, name, binds) do
+ type_casted_binds = binds.map { |col, val|
+ [col, type_cast(val, col)]
+ }
+
+ log(sql, name, type_casted_binds) do
if binds.empty?
stmt = @connection.prepare(sql)
else
@@ -487,7 +485,7 @@ module ActiveRecord
end
begin
- stmt.execute(*binds.map { |col, val| type_cast(val, col) })
+ stmt.execute(*type_casted_binds.map { |_, val| val })
rescue Mysql::Error => e
# Older versions of MySQL leave the prepared statement in a bad
# place when an error occurs. To support older mysql versions, we
@@ -559,7 +557,7 @@ module ActiveRecord
def set_field_encoding field_name
field_name.force_encoding(client_encoding)
if internal_enc = Encoding.default_internal
- field_name = field_name.encoding(internal_enc)
+ field_name = field_name.encode!(internal_enc)
end
field_name
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
index dab876af14..6c5792954f 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
@@ -234,6 +234,10 @@ module ActiveRecord
ConnectionAdapters::PostgreSQLColumn.string_to_hstore value
end
+
+ def accessor
+ ActiveRecord::Store::StringKeyedHashAccessor
+ end
end
class Cidr < Type
@@ -245,11 +249,19 @@ module ActiveRecord
end
class Json < Type
+ def type_cast_for_write(value)
+ ConnectionAdapters::PostgreSQLColumn.json_to_string value
+ end
+
def type_cast(value)
return if value.nil?
ConnectionAdapters::PostgreSQLColumn.string_to_json value
end
+
+ def accessor
+ ActiveRecord::Store::StringKeyedHashAccessor
+ end
end
class TypeMap
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
index e9daa5d7ff..c1f978a081 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
@@ -86,8 +86,11 @@ module ActiveRecord
case value
when Range
- return super(value, column) unless /range$/ =~ column.sql_type
- PostgreSQLColumn.range_to_string(value)
+ if /range$/ =~ column.sql_type
+ PostgreSQLColumn.range_to_string(value)
+ else
+ super(value, column)
+ end
when NilClass
if column.array && array_member
'NULL'
@@ -101,12 +104,21 @@ module ActiveRecord
when 'point' then PostgreSQLColumn.point_to_string(value)
when 'json' then PostgreSQLColumn.json_to_string(value)
else
- return super(value, column) unless column.array
- PostgreSQLColumn.array_to_string(value, column, self)
+ if column.array
+ PostgreSQLColumn.array_to_string(value, column, self)
+ else
+ super(value, column)
+ end
end
when String
- return super(value, column) unless 'bytea' == column.sql_type
- { :value => value, :format => 1 }
+ if 'bytea' == column.sql_type
+ # Return a bind param hash with format as binary.
+ # See http://deveiate.org/code/pg/PGconn.html#method-i-exec_prepared-doc
+ # for more information
+ { value: value, format: 1 }
+ else
+ super(value, column)
+ end
when Hash
case column.sql_type
when 'hstore' then PostgreSQLColumn.hstore_to_string(value)
@@ -114,8 +126,11 @@ module ActiveRecord
else super(value, column)
end
when IPAddr
- return super(value, column) unless ['inet','cidr'].include? column.sql_type
- PostgreSQLColumn.cidr_to_string(value)
+ if %w(inet cidr).include? column.sql_type
+ PostgreSQLColumn.cidr_to_string(value)
+ else
+ super(value, column)
+ end
else
super(value, column)
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
index 3fce8de1ba..5dc70a5ad1 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
@@ -46,7 +46,7 @@ module ActiveRecord
end
# Create a new PostgreSQL database. Options include <tt>:owner</tt>, <tt>:template</tt>,
- # <tt>:encoding</tt>, <tt>:collation</tt>, <tt>:ctype</tt>,
+ # <tt>:encoding</tt> (defaults to utf8), <tt>:collation</tt>, <tt>:ctype</tt>,
# <tt>:tablespace</tt>, and <tt>:connection_limit</tt> (note that MySQL uses
# <tt>:charset</tt> while PostgreSQL uses <tt>:encoding</tt>).
#
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
index 771a150eae..adeb57d913 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -148,10 +148,14 @@ module ActiveRecord
@oid_type.type_cast value
end
+ def accessor
+ @oid_type.accessor
+ end
+
private
def has_default_function?(default_value, default)
- !default_value && (%r{\w+(.*)} === default)
+ !default_value && (%r{\w+\(.*\)} === default)
end
def extract_limit(sql_type)
@@ -627,7 +631,6 @@ module ActiveRecord
true
end
- # Returns true.
def supports_explain?
true
end
@@ -779,11 +782,12 @@ module ActiveRecord
def exec_cache(sql, name, binds)
stmt_key = prepare_statement(sql)
+ type_casted_binds = binds.map { |col, val|
+ [col, type_cast(val, col)]
+ }
- log(sql, name, binds, stmt_key) do
- @connection.send_query_prepared(stmt_key, binds.map { |col, val|
- type_cast(val, col)
- })
+ log(sql, name, type_casted_binds, stmt_key) do
+ @connection.send_query_prepared(stmt_key, type_casted_binds.map { |_, val| val })
@connection.block
@connection.get_last_result
end
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
index 997384daea..2cf1015f2c 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
@@ -55,6 +55,21 @@ module ActiveRecord
class SQLite3Adapter < AbstractAdapter
include Savepoints
+ NATIVE_DATABASE_TYPES = {
+ primary_key: 'INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL',
+ string: { name: "varchar", limit: 255 },
+ text: { name: "text" },
+ integer: { name: "integer" },
+ float: { name: "float" },
+ decimal: { name: "decimal" },
+ datetime: { name: "datetime" },
+ timestamp: { name: "datetime" },
+ time: { name: "time" },
+ date: { name: "date" },
+ binary: { name: "blob" },
+ boolean: { name: "boolean" }
+ }
+
class Version
include Comparable
@@ -126,14 +141,12 @@ module ActiveRecord
'SQLite'
end
- # Returns true
def supports_ddl_transactions?
true
end
- # Returns true if SQLite version is '3.6.8' or greater, false otherwise.
def supports_savepoints?
- sqlite_version >= '3.6.8'
+ true
end
# Returns true, since this connection adapter supports prepared statement
@@ -147,7 +160,6 @@ module ActiveRecord
true
end
- # Returns true.
def supports_primary_key? #:nodoc:
true
end
@@ -156,7 +168,6 @@ module ActiveRecord
true
end
- # Returns true
def supports_add_column?
true
end
@@ -178,16 +189,6 @@ module ActiveRecord
@statements.clear
end
- # Returns true
- def supports_count_distinct? #:nodoc:
- true
- end
-
- # Returns true
- def supports_autoincrement? #:nodoc:
- true
- end
-
def supports_index_sort_order?
true
end
@@ -200,20 +201,7 @@ module ActiveRecord
end
def native_database_types #:nodoc:
- {
- :primary_key => default_primary_key_type,
- :string => { :name => "varchar", :limit => 255 },
- :text => { :name => "text" },
- :integer => { :name => "integer" },
- :float => { :name => "float" },
- :decimal => { :name => "decimal" },
- :datetime => { :name => "datetime" },
- :timestamp => { :name => "datetime" },
- :time => { :name => "time" },
- :date => { :name => "date" },
- :binary => { :name => "blob" },
- :boolean => { :name => "boolean" }
- }
+ NATIVE_DATABASE_TYPES
end
# Returns the current database encoding format as a string, eg: 'UTF-8'
@@ -221,7 +209,6 @@ module ActiveRecord
@connection.encoding.to_s
end
- # Returns true.
def supports_explain?
true
end
@@ -294,8 +281,11 @@ module ActiveRecord
end
def exec_query(sql, name = nil, binds = [])
- log(sql, name, binds) do
+ type_casted_binds = binds.map { |col, val|
+ [col, type_cast(val, col)]
+ }
+ log(sql, name, type_casted_binds) do
# Don't cache statements if they are not prepared
if without_prepared_statement?(binds)
stmt = @connection.prepare(sql)
@@ -310,9 +300,7 @@ module ActiveRecord
stmt = cache[:stmt]
cols = cache[:cols] ||= stmt.columns
stmt.reset!
- stmt.bind_params binds.map { |col, val|
- type_cast(val, col)
- }
+ stmt.bind_params type_casted_binds.map { |_, val| val }
end
ActiveRecord::Result.new(cols, stmt.to_a)
@@ -596,14 +584,6 @@ module ActiveRecord
@sqlite_version ||= SQLite3Adapter::Version.new(select_value('select sqlite_version(*)'))
end
- def default_primary_key_type
- if supports_autoincrement?
- 'INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL'
- else
- 'INTEGER PRIMARY KEY NOT NULL'
- end
- end
-
def translate_exception(exception, message)
case exception.message
when /column(s)? .* (is|are) not unique/
diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb
index 366ebde418..96b5686ae0 100644
--- a/activerecord/lib/active_record/core.rb
+++ b/activerecord/lib/active_record/core.rb
@@ -91,12 +91,12 @@ module ActiveRecord
def initialize_generated_modules
super
- generated_feature_methods
+ generated_association_methods
end
- def generated_feature_methods
- @generated_feature_methods ||= begin
- mod = const_set(:GeneratedFeatureMethods, Module.new)
+ def generated_association_methods
+ @generated_association_methods ||= begin
+ mod = const_set(:GeneratedAssociationMethods, Module.new)
include mod
mod
end
@@ -109,7 +109,7 @@ module ActiveRecord
elsif abstract_class?
"#{super}(abstract)"
elsif !connected?
- "#{super}(no database connection)"
+ "#{super} (call '#{super}.connection' to establish a connection)"
elsif table_exists?
attr_list = columns.map { |c| "#{c.name}: #{c.type}" } * ', '
"#{super}(#{attr_list})"
@@ -310,6 +310,15 @@ module ActiveRecord
@attributes.frozen?
end
+ # Allows sort on objects
+ def <=>(other_object)
+ if other_object.is_a?(self.class)
+ self.to_key <=> other_object.to_key
+ else
+ super
+ end
+ end
+
# Returns +true+ if the record is read only. Records loaded through joins with piggy-back
# attributes will be marked as read only since they cannot be saved.
def readonly?
diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb
new file mode 100644
index 0000000000..5fcc0382d8
--- /dev/null
+++ b/activerecord/lib/active_record/enum.rb
@@ -0,0 +1,88 @@
+module ActiveRecord
+ # Declare an enum attribute where the values map to integers in the database, but can be queried by name. Example:
+ #
+ # class Conversation < ActiveRecord::Base
+ # enum status: [ :active, :archived ]
+ # end
+ #
+ # # conversation.update! status: 0
+ # conversation.active!
+ # conversation.active? # => true
+ # conversation.status # => "active"
+ #
+ # # conversation.update! status: 1
+ # conversation.archived!
+ # conversation.archived? # => true
+ # conversation.status # => "archived"
+ #
+ # # conversation.update! status: 1
+ # conversation.status = "archived"
+ #
+ # You can set the default value from the database declaration, like:
+ #
+ # create_table :conversations do |t|
+ # t.column :status, :integer, default: 0
+ # end
+ #
+ # Good practice is to let the first declared status be the default.
+ #
+ # Finally, it's also possible to explicitly map the relation between attribute and database integer:
+ #
+ # class Conversation < ActiveRecord::Base
+ # enum status: { active: 0, archived: 1 }
+ # end
+ #
+ # In rare circumstances you might need to access the mapping directly.
+ # The mappings are exposed through a constant with the attributes name:
+ #
+ # Conversation::STATUS # => { "active" => 0, "archived" => 1 }
+ #
+ # Use that constant when you need to know the ordinal value of an enum:
+ #
+ # Conversation.where("status <> ?", Conversation::STATUS[:archived])
+ module Enum
+ def enum(definitions)
+ klass = self
+ definitions.each do |name, values|
+ # DIRECTION = { }
+ enum_values = _enum_methods_module.const_set name.to_s.upcase, ActiveSupport::HashWithIndifferentAccess.new
+ name = name.to_sym
+
+ _enum_methods_module.module_eval do
+ # def direction=(value) self[:direction] = DIRECTION[value] end
+ define_method("#{name}=") { |value|
+ unless enum_values.has_key?(value)
+ raise ArgumentError, "'#{value}' is not a valid #{name}"
+ end
+ self[name] = enum_values[value]
+ }
+
+ # def direction() DIRECTION.key self[:direction] end
+ define_method(name) { enum_values.key self[name] }
+
+ pairs = values.respond_to?(:each_pair) ? values.each_pair : values.each_with_index
+ pairs.each do |value, i|
+ enum_values[value] = i
+
+ # scope :incoming, -> { where direction: 0 }
+ klass.scope value, -> { klass.where name => i }
+
+ # def incoming?() direction == 0 end
+ define_method("#{value}?") { self[name] == i }
+
+ # def incoming! update! direction: :incoming end
+ define_method("#{value}!") { update! name => value }
+ end
+ end
+ end
+ end
+
+ def _enum_methods_module
+ @_enum_methods_module ||= begin
+ mod = Module.new
+ include mod
+ mod
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb
index 3bb3131bd1..8601414209 100644
--- a/activerecord/lib/active_record/fixtures.rb
+++ b/activerecord/lib/active_record/fixtures.rb
@@ -504,16 +504,8 @@ module ActiveRecord
connection.transaction(:requires_new => true) do
fixture_sets.each do |fs|
conn = fs.model_class.respond_to?(:connection) ? fs.model_class.connection : connection
- table_rows = fs.table_rows
-
- table_rows.keys.each do |table|
- conn.delete "DELETE FROM #{conn.quote_table_name(table)}", 'Fixture Delete'
- end
-
- table_rows.each do |fixture_set_name, rows|
- rows.each do |row|
- conn.insert_fixture(row, fixture_set_name)
- end
+ fs.fixture_sql(conn).each do |stmt|
+ conn.execute stmt
end
end
@@ -580,6 +572,16 @@ module ActiveRecord
fixtures.size
end
+ def fixture_sql(conn)
+ table_rows = self.table_rows
+
+ table_rows.keys.map { |table|
+ "DELETE FROM #{conn.quote_table_name(table)}"
+ }.concat table_rows.flat_map { |fixture_set_name, rows|
+ rows.map { |row| conn.fixture_sql(row, fixture_set_name) }
+ }
+ end
+
# Return a hash of rows to be inserted. The key is the table, the value is
# a list of rows to insert to that table.
def table_rows
diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb
index 2589b2f3da..27576b1e61 100644
--- a/activerecord/lib/active_record/integration.rb
+++ b/activerecord/lib/active_record/integration.rb
@@ -1,3 +1,5 @@
+require 'active_support/core_ext/string/filters'
+
module ActiveRecord
module Integration
extend ActiveSupport::Concern
@@ -45,10 +47,19 @@ module ActiveRecord
# Product.new.cache_key # => "products/new"
# Product.find(5).cache_key # => "products/5" (updated_at not available)
# Person.find(5).cache_key # => "people/5-20071224150000" (updated_at available)
- def cache_key
+ #
+ # You can also pass a list of named timestamps, and the newest in the list will be
+ # used to generate the key:
+ #
+ # Person.find(5).cache_key(:updated_at, :last_reviewed_at)
+ def cache_key(*timestamp_names)
case
when new_record?
"#{self.class.model_name.cache_key}/new"
+ when timestamp_names.any?
+ timestamp = max_updated_column_timestamp(timestamp_names)
+ timestamp = timestamp.utc.to_s(cache_timestamp_format)
+ "#{self.class.model_name.cache_key}/#{id}-#{timestamp}"
when timestamp = max_updated_column_timestamp
timestamp = timestamp.utc.to_s(cache_timestamp_format)
"#{self.class.model_name.cache_key}/#{id}-#{timestamp}"
@@ -56,5 +67,45 @@ module ActiveRecord
"#{self.class.model_name.cache_key}/#{id}"
end
end
+
+ module ClassMethods
+ # Defines your model's +to_param+ method to generate "pretty" URLs
+ # using +method_name+, which can be any attribute or method that
+ # responds to +to_s+.
+ #
+ # class User < ActiveRecord::Base
+ # to_param :name
+ # end
+ #
+ # user = User.find_by(name: 'Fancy Pants')
+ # user.id # => 123
+ # user_path(user) # => "/users/123-fancy-pants"
+ #
+ # Values longer than 20 characters will be truncated. The value
+ # is truncated word by word.
+ #
+ # user = User.find_by(name: 'David HeinemeierHansson')
+ # user.id # => 125
+ # user_path(user) # => "/users/125-david"
+ #
+ # Because the generated param begins with the record's +id+, it is
+ # suitable for passing to +find+. In a controller, for example:
+ #
+ # params[:id] # => "123-fancy-pants"
+ # User.find(params[:id]).id # => 123
+ def to_param(method_name = nil)
+ if method_name.nil?
+ super()
+ else
+ define_method :to_param do
+ if (default = super()) && (result = send(method_name).to_s).present?
+ "#{default}-#{result.squish.truncate(20, separator: /\s/, omission: nil).parameterize}"
+ else
+ default
+ end
+ end
+ end
+ end
+ end
end
end
diff --git a/activerecord/lib/active_record/locking/pessimistic.rb b/activerecord/lib/active_record/locking/pessimistic.rb
index ddf2afca0c..ff7102d35b 100644
--- a/activerecord/lib/active_record/locking/pessimistic.rb
+++ b/activerecord/lib/active_record/locking/pessimistic.rb
@@ -3,12 +3,12 @@ module ActiveRecord
# Locking::Pessimistic provides support for row-level locking using
# SELECT ... FOR UPDATE and other lock types.
#
- # Pass <tt>lock: true</tt> to <tt>ActiveRecord::Base.find</tt> to obtain an exclusive
+ # Chain <tt>ActiveRecord::Base#find</tt> to <tt>ActiveRecord::QueryMethods#lock</tt> to obtain an exclusive
# lock on the selected rows:
# # select * from accounts where id=1 for update
- # Account.find(1, lock: true)
+ # Account.lock.find(1)
#
- # Pass <tt>lock: 'some locking clause'</tt> to give a database-specific locking clause
+ # Call <tt>lock('some locking clause')</tt> to use a database-specific locking clause
# of your own such as 'LOCK IN SHARE MODE' or 'FOR UPDATE NOWAIT'. Example:
#
# Account.transaction do
diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb
index 927fbab8f0..654ef21b07 100644
--- a/activerecord/lib/active_record/log_subscriber.rb
+++ b/activerecord/lib/active_record/log_subscriber.rb
@@ -23,6 +23,8 @@ module ActiveRecord
def render_bind(column, value)
if column
if column.binary?
+ # This specifically deals with the PG adapter that casts bytea columns into a Hash.
+ value = value[:value] if value.is_a?(Hash)
value = "<#{value.bytesize} bytes of binary data>"
end
diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb
index a1ad4f6255..d010f23517 100644
--- a/activerecord/lib/active_record/migration.rb
+++ b/activerecord/lib/active_record/migration.rb
@@ -32,7 +32,11 @@ module ActiveRecord
class PendingMigrationError < ActiveRecordError#:nodoc:
def initialize
- super("Migrations are pending; run 'bin/rake db:migrate RAILS_ENV=#{::Rails.env}' to resolve this issue.")
+ if defined?(Rails)
+ super("Migrations are pending; run 'bin/rake db:migrate RAILS_ENV=#{::Rails.env}' to resolve this issue.")
+ else
+ super("Migrations are pending; run 'bin/rake db:migrate' to resolve this issue.")
+ end
end
end
@@ -120,8 +124,8 @@ module ActiveRecord
# a column but keeps the type and content.
# * <tt>change_column(table_name, column_name, type, options)</tt>: Changes
# the column to a different type using the same parameters as add_column.
- # * <tt>remove_column(table_name, column_names)</tt>: Removes the column listed in
- # +column_names+ from the table called +table_name+.
+ # * <tt>remove_column(table_name, column_name, type, options)</tt>: Removes the column
+ # named +column_name+ from the table called +table_name+.
# * <tt>add_index(table_name, column_names, options)</tt>: Adds a new index
# with the name of the column. Other options include
# <tt>:name</tt>, <tt>:unique</tt> (e.g.
@@ -629,7 +633,7 @@ module ActiveRecord
def copy(destination, sources, options = {})
copied = []
- FileUtils.mkdir_p(destination) unless File.exists?(destination)
+ FileUtils.mkdir_p(destination) unless File.exist?(destination)
destination_migrations = ActiveRecord::Migrator.migrations(destination)
last = destination_migrations.last
diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb
index 75c0c1bda8..dc5ff02882 100644
--- a/activerecord/lib/active_record/model_schema.rb
+++ b/activerecord/lib/active_record/model_schema.rb
@@ -34,6 +34,12 @@ module ActiveRecord
##
# :singleton-method:
+ # Accessor for the name of the schema migrations table. By default, the value is "schema_migrations"
+ class_attribute :schema_migrations_table_name, instance_accessor: false
+ self.schema_migrations_table_name = "schema_migrations"
+
+ ##
+ # :singleton-method:
# Indicates whether table names should be the pluralized versions of the corresponding class names.
# If true, the default table name for a Product class will be +products+. If false, it would just be +product+.
# See table_name for the full rules on table/class naming. This is true, by default.
diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb
index df28451bb7..9d92e747d4 100644
--- a/activerecord/lib/active_record/nested_attributes.rb
+++ b/activerecord/lib/active_record/nested_attributes.rb
@@ -335,7 +335,7 @@ module ActiveRecord
# the helper methods defined below. Makes it seem like the nested
# associations are just regular associations.
def generate_association_writer(association_name, type)
- generated_feature_methods.module_eval <<-eoruby, __FILE__, __LINE__ + 1
+ generated_association_methods.module_eval <<-eoruby, __FILE__, __LINE__ + 1
if method_defined?(:#{association_name}_attributes=)
remove_method(:#{association_name}_attributes=)
end
diff --git a/activerecord/lib/active_record/no_touching.rb b/activerecord/lib/active_record/no_touching.rb
new file mode 100644
index 0000000000..dbf4564ae5
--- /dev/null
+++ b/activerecord/lib/active_record/no_touching.rb
@@ -0,0 +1,52 @@
+module ActiveRecord
+ # = Active Record No Touching
+ module NoTouching
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ # Lets you selectively disable calls to `touch` for the
+ # duration of a block.
+ #
+ # ==== Examples
+ # ActiveRecord::Base.no_touching do
+ # Project.first.touch # does nothing
+ # Message.first.touch # does nothing
+ # end
+ #
+ # Project.no_touching do
+ # Project.first.touch # does nothing
+ # Message.first.touch # works, but does not touch the associated project
+ # end
+ #
+ def no_touching(&block)
+ NoTouching.apply_to(self, &block)
+ end
+ end
+
+ class << self
+ def apply_to(klass) #:nodoc:
+ klasses.push(klass)
+ yield
+ ensure
+ klasses.pop
+ end
+
+ def applied_to?(klass) #:nodoc:
+ klasses.any? { |k| k >= klass }
+ end
+
+ private
+ def klasses
+ Thread.current[:no_touching_classes] ||= []
+ end
+ end
+
+ def no_touching?
+ NoTouching.applied_to?(self.class)
+ end
+
+ def touch(*)
+ super unless no_touching?
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/null_relation.rb b/activerecord/lib/active_record/null_relation.rb
index 1f3d377e53..080b20134d 100644
--- a/activerecord/lib/active_record/null_relation.rb
+++ b/activerecord/lib/active_record/null_relation.rb
@@ -6,7 +6,7 @@ module ActiveRecord
@records = []
end
- def pluck(_column_name)
+ def pluck(*column_names)
[]
end
diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb
index 6bee4f38e7..fd4c973504 100644
--- a/activerecord/lib/active_record/querying.rb
+++ b/activerecord/lib/active_record/querying.rb
@@ -7,7 +7,7 @@ module ActiveRecord
delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, to: :all
delegate :find_each, :find_in_batches, to: :all
delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins,
- :where, :preload, :eager_load, :includes, :from, :lock, :readonly,
+ :where, :rewhere, :preload, :eager_load, :includes, :from, :lock, :readonly,
:having, :create_with, :uniq, :distinct, :references, :none, :unscope, to: :all
delegate :count, :average, :minimum, :maximum, :sum, :calculate, to: :all
delegate :pluck, :ids, to: :all
diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake
index 26770bd951..3ca66223b6 100644
--- a/activerecord/lib/active_record/railties/databases.rake
+++ b/activerecord/lib/active_record/railties/databases.rake
@@ -287,7 +287,7 @@ db_namespace = namespace :db do
desc 'Clear a db/schema_cache.dump file.'
task :clear => [:environment, :load_config] do
filename = File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema_cache.dump")
- FileUtils.rm(filename) if File.exists?(filename)
+ FileUtils.rm(filename) if File.exist?(filename)
end
end
diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb
index e88c5d17cb..bce7766501 100644
--- a/activerecord/lib/active_record/reflection.rb
+++ b/activerecord/lib/active_record/reflection.rb
@@ -574,7 +574,7 @@ module ActiveRecord
# Add to it the scope from this reflection (if any)
scope_chain.first << scope if scope
- through_scope_chain = through_reflection.scope_chain
+ through_scope_chain = through_reflection.scope_chain.map(&:dup)
if options[:source_type]
through_scope_chain.first <<
diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb
index cfaf566ec4..ff088818ee 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
@@ -244,8 +244,13 @@ module ActiveRecord
def empty?
return @records.empty? if loaded?
- c = count(:all)
- c.respond_to?(:zero?) ? c.zero? : c.empty?
+ if limit_value == 0
+ true
+ else
+ # FIXME: This count is not compatible with #select('authors.*') or other select narrows
+ c = count
+ c.respond_to?(:zero?) ? c.zero? : c.empty?
+ end
end
# Returns true if there are any records.
@@ -507,8 +512,7 @@ module ActiveRecord
visitor = connection.visitor
if eager_loading?
- join_dependency = construct_join_dependency
- relation = construct_relation_for_association_find(join_dependency)
+ find_with_associations { |rel| relation = rel }
end
ast = relation.arel.ast
diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb
index 8583286de5..d91d6367a3 100644
--- a/activerecord/lib/active_record/relation/finder_methods.rb
+++ b/activerecord/lib/active_record/relation/finder_methods.rb
@@ -6,11 +6,12 @@ module ActiveRecord
# If no record can be found for all of the listed ids, then RecordNotFound will be raised. If the primary key
# is an integer, find by id coerces its arguments using +to_i+.
#
- # Person.find(1) # returns the object for ID = 1
- # Person.find("1") # returns the object for ID = 1
- # Person.find(1, 2, 6) # returns an array for objects with IDs in (1, 2, 6)
- # Person.find([7, 17]) # returns an array for objects with IDs in (7, 17)
- # Person.find([1]) # returns an array for the object with ID = 1
+ # Person.find(1) # returns the object for ID = 1
+ # Person.find("1") # returns the object for ID = 1
+ # Person.find("31-sarah") # returns the object for ID = 31
+ # Person.find(1, 2, 6) # returns an array for objects with IDs in (1, 2, 6)
+ # Person.find([7, 17]) # returns an array for objects with IDs in (7, 17)
+ # Person.find([1]) # returns an array for the object with ID = 1
# Person.where("administrator = 1").order("created_on DESC").find(1)
#
# <tt>ActiveRecord::RecordNotFound</tt> will be raised if one or more ids are not found.
@@ -201,7 +202,7 @@ module ActiveRecord
conditions = conditions.id if Base === conditions
return false if !conditions
- relation = construct_relation_for_association_find(construct_join_dependency)
+ relation = apply_join_dependency(self, construct_join_dependency)
return false if ActiveRecord::NullRelation === relation
relation = relation.except(:select, :order).select(ONE_AS_ONE).limit(1)
@@ -242,17 +243,25 @@ module ActiveRecord
def find_with_associations
join_dependency = construct_join_dependency
- relation = construct_relation_for_association_find(join_dependency)
- if ActiveRecord::NullRelation === relation
- []
+
+ aliases = join_dependency.aliases
+ relation = select aliases.columns
+ relation = apply_join_dependency(relation, join_dependency)
+
+ if block_given?
+ yield relation
else
- rows = connection.select_all(relation.arel, 'SQL', relation.bind_values.dup)
- join_dependency.instantiate(rows)
+ if ActiveRecord::NullRelation === relation
+ []
+ else
+ rows = connection.select_all(relation.arel, 'SQL', relation.bind_values.dup)
+ join_dependency.instantiate(rows, aliases)
+ end
end
end
def construct_join_dependency(joins = [])
- including = (eager_load_values + includes_values).uniq
+ including = eager_load_values + includes_values
ActiveRecord::Associations::JoinDependency.new(@klass, including, joins)
end
@@ -260,11 +269,6 @@ module ActiveRecord
apply_join_dependency(self, construct_join_dependency(arel.froms.first))
end
- def construct_relation_for_association_find(join_dependency)
- relation = except(:select).select(join_dependency.columns + select_values)
- apply_join_dependency(relation, join_dependency)
- end
-
def apply_join_dependency(relation, join_dependency)
relation = relation.except(:includes, :eager_load, :preload)
relation = relation.joins join_dependency
@@ -297,6 +301,8 @@ module ActiveRecord
protected
def find_with_ids(*ids)
+ raise UnknownPrimaryKey.new(@klass) if primary_key.nil?
+
expects_array = ids.first.kind_of?(Array)
return ids.first if expects_array && ids.first.empty?
diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb
index 9c9690215a..62c555f6d6 100644
--- a/activerecord/lib/active_record/relation/query_methods.rb
+++ b/activerecord/lib/active_record/relation/query_methods.rb
@@ -341,9 +341,6 @@ module ActiveRecord
# User.where(name: "John", active: true).unscope(where: :name)
# == User.where(active: true)
#
- # This method is applied before the default_scope is applied. So the conditions
- # specified in default_scope will not be removed.
- #
# Note that this method is more generalized than ActiveRecord::SpawnMethods#except
# because #except will only affect a particular relation's values. It won't wipe
# the order, grouping, etc. when that relation is merged. For example:
@@ -553,6 +550,18 @@ module ActiveRecord
end
end
+ # Allows you to change a previously set where condition for a given attribute, instead of appending to that condition.
+ #
+ # Post.where(trashed: true).where(trashed: false) #=> WHERE `trashed` = 1 AND `trashed` = 0
+ # Post.where(trashed: true).rewhere(trashed: false) #=> WHERE `trashed` = 0
+ # Post.where(active: true).where(trashed: true).rewhere(trashed: false) #=> WHERE `active` = 1 AND `trashed` = 0
+ #
+ # This is short-hand for unscope(where: conditions.keys).where(conditions). Note that unlike reorder, we're only unscoping
+ # the named conditions -- not the entire where statement.
+ def rewhere(conditions)
+ unscope(where: conditions.keys).where(conditions)
+ end
+
# Allows to specify a HAVING clause. Note that you can't use HAVING
# without also specifying a GROUP clause.
#
@@ -856,7 +865,7 @@ module ActiveRecord
where_values.reject! do |rel|
case rel
- when Arel::Nodes::In, Arel::Nodes::Equality
+ when Arel::Nodes::In, Arel::Nodes::NotIn, Arel::Nodes::Equality, Arel::Nodes::NotEqual
subrelation = (rel.left.kind_of?(Arel::Attributes::Attribute) ? rel.left : rel.right)
subrelation.name.to_sym == target_value_sym
else
@@ -894,6 +903,13 @@ module ActiveRecord
def build_where(opts, other = [])
case opts
when String, Array
+ #TODO: Remove duplication with: /activerecord/lib/active_record/sanitization.rb:113
+ values = Hash === other.first ? other.first.values : other
+
+ values.grep(ActiveRecord::Relation) do |rel|
+ self.bind_values += rel.bind_values
+ end
+
[@klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))]
when Hash
opts = PredicateBuilder.resolve_column_aliases(klass, opts)
@@ -950,11 +966,7 @@ module ActiveRecord
join_list
)
- stashed_association_joins.each do |dep|
- join_dependency.merge_outer_joins! dep
- end
-
- joins = join_dependency.join_constraints
+ joins = join_dependency.join_constraints stashed_association_joins
joins.each { |join| manager.from(join) }
diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb
index 1dc3bf3f12..469451e2f4 100644
--- a/activerecord/lib/active_record/result.rb
+++ b/activerecord/lib/active_record/result.rb
@@ -83,9 +83,10 @@ module ActiveRecord
end
def initialize_copy(other)
- @columns = columns.dup
- @rows = rows.dup
- @hash_rows = nil
+ @columns = columns.dup
+ @rows = rows.dup
+ @column_types = column_types.dup
+ @hash_rows = nil
end
private
diff --git a/activerecord/lib/active_record/runtime_registry.rb b/activerecord/lib/active_record/runtime_registry.rb
index 63e6738622..9d605b826a 100644
--- a/activerecord/lib/active_record/runtime_registry.rb
+++ b/activerecord/lib/active_record/runtime_registry.rb
@@ -13,5 +13,10 @@ module ActiveRecord
extend ActiveSupport::PerThreadRegistry
attr_accessor :connection_handler, :sql_runtime, :connection_id
+
+ [:connection_handler, :sql_runtime, :connection_id].each do |val|
+ class_eval %{ def self.#{val}; instance.#{val}; end }, __FILE__, __LINE__
+ class_eval %{ def self.#{val}=(x); instance.#{val}=x; end }, __FILE__, __LINE__
+ end
end
end
diff --git a/activerecord/lib/active_record/schema_migration.rb b/activerecord/lib/active_record/schema_migration.rb
index fee19b1096..a9d164e366 100644
--- a/activerecord/lib/active_record/schema_migration.rb
+++ b/activerecord/lib/active_record/schema_migration.rb
@@ -7,11 +7,11 @@ module ActiveRecord
class << self
def table_name
- "#{table_name_prefix}schema_migrations#{table_name_suffix}"
+ "#{table_name_prefix}#{ActiveRecord::Base.schema_migrations_table_name}#{table_name_suffix}"
end
def index_name
- "#{table_name_prefix}unique_schema_migrations#{table_name_suffix}"
+ "#{table_name_prefix}unique_#{ActiveRecord::Base.schema_migrations_table_name}#{table_name_suffix}"
end
def table_exists?
diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb
index a610f479f2..186a737561 100644
--- a/activerecord/lib/active_record/store.rb
+++ b/activerecord/lib/active_record/store.rb
@@ -15,6 +15,11 @@ module ActiveRecord
# You can set custom coder to encode/decode your serialized attributes to/from different formats.
# JSON, YAML, Marshal are supported out of the box. Generally it can be any wrapper that provides +load+ and +dump+.
#
+ # NOTE - If you are using PostgreSQL specific columns like +hstore+ or +json+ there is no need for
+ # the serialization provided by +store+. Simply use +store_accessor+ instead to generate
+ # the accessor methods. Be aware that these columns use a string keyed hash and do not allow access
+ # using a symbol.
+ #
# Examples:
#
# class User < ActiveRecord::Base
@@ -86,6 +91,9 @@ module ActiveRecord
end
end
+ # assign new store attribute and create new hash to ensure that each class in the hierarchy
+ # has its own hash of stored attributes.
+ self.stored_attributes = {} if self.stored_attributes.blank?
self.stored_attributes[store_attribute] ||= []
self.stored_attributes[store_attribute] |= keys
end
@@ -101,26 +109,58 @@ module ActiveRecord
protected
def read_store_attribute(store_attribute, key)
- attribute = initialize_store_attribute(store_attribute)
- attribute[key]
+ accessor = store_accessor_for(store_attribute)
+ accessor.read(self, store_attribute, key)
end
def write_store_attribute(store_attribute, key, value)
- attribute = initialize_store_attribute(store_attribute)
- if value != attribute[key]
- send :"#{store_attribute}_will_change!"
- attribute[key] = value
- end
+ accessor = store_accessor_for(store_attribute)
+ accessor.write(self, store_attribute, key, value)
end
private
- def initialize_store_attribute(store_attribute)
- attribute = send(store_attribute)
- unless attribute.is_a?(ActiveSupport::HashWithIndifferentAccess)
- attribute = IndifferentCoder.as_indifferent_hash(attribute)
- send :"#{store_attribute}=", attribute
+ def store_accessor_for(store_attribute)
+ @column_types[store_attribute.to_s].accessor
+ end
+
+ class HashAccessor
+ def self.read(object, attribute, key)
+ prepare(object, attribute)
+ object.public_send(attribute)[key]
+ end
+
+ def self.write(object, attribute, key, value)
+ prepare(object, attribute)
+ if value != read(object, attribute, key)
+ object.public_send :"#{attribute}_will_change!"
+ object.public_send(attribute)[key] = value
+ end
+ end
+
+ def self.prepare(object, attribute)
+ object.public_send :"#{attribute}=", {} unless object.send(attribute)
+ end
+ end
+
+ class StringKeyedHashAccessor < HashAccessor
+ def self.read(object, attribute, key)
+ super object, attribute, key.to_s
+ end
+
+ def self.write(object, attribute, key, value)
+ super object, attribute, key.to_s, value
+ end
+ end
+
+ class IndifferentHashAccessor < ActiveRecord::Store::HashAccessor
+ def self.prepare(object, store_attribute)
+ attribute = object.send(store_attribute)
+ unless attribute.is_a?(ActiveSupport::HashWithIndifferentAccess)
+ attribute = IndifferentCoder.as_indifferent_hash(attribute)
+ object.send :"#{store_attribute}=", attribute
+ end
+ attribute
end
- attribute
end
class IndifferentCoder # :nodoc:
@@ -138,7 +178,7 @@ module ActiveRecord
end
def load(yaml)
- self.class.as_indifferent_hash @coder.load(yaml)
+ self.class.as_indifferent_hash(@coder.load(yaml))
end
def self.as_indifferent_hash(obj)
diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb
index 8acf0bd520..0b6a49ac85 100644
--- a/activerecord/lib/active_record/tasks/database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/database_tasks.rb
@@ -149,7 +149,7 @@ module ActiveRecord
end
def check_schema_file(filename)
- unless File.exists?(filename)
+ unless File.exist?(filename)
message = %{#{filename} doesn't exist yet. Run `rake db:migrate` to create it, then try again.}
message << %{ If you do not intend to use a database, you should instead alter #{Rails.root}/config/application.rb to limit the frameworks that will be loaded.} if defined?(::Rails)
Kernel.abort message
diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
index 50569d2462..c755831e6d 100644
--- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
@@ -134,8 +134,9 @@ IDENTIFIED BY '#{configuration['password']}' WITH GRANT OPTION;
args << "--password=#{configuration['password']}" if configuration['password']
args.concat(['--default-character-set', configuration['encoding']]) if configuration['encoding']
configuration.slice('host', 'port', 'socket').each do |k, v|
- args.concat([ "--#{k}", v ]) if v
+ args.concat([ "--#{k}", v.to_s ]) if v
end
+
args
end
end
diff --git a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb
index 4413330fab..3d02ee07d0 100644
--- a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb
@@ -59,7 +59,7 @@ module ActiveRecord
def structure_load(filename)
set_psql_env
- Kernel.system("psql -q -f #{filename} #{configuration['database']}")
+ Kernel.system("psql -q -f #{Shellwords.escape(filename)} #{configuration['database']}")
end
private
diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb
index 9253150c4f..e0541b7681 100644
--- a/activerecord/lib/active_record/timestamp.rb
+++ b/activerecord/lib/active_record/timestamp.rb
@@ -98,8 +98,8 @@ module ActiveRecord
timestamp_attributes_for_create + timestamp_attributes_for_update
end
- def max_updated_column_timestamp
- if (timestamps = timestamp_attributes_for_update.map { |attr| self[attr] }.compact).present?
+ def max_updated_column_timestamp(timestamp_names = timestamp_attributes_for_update)
+ if (timestamps = timestamp_names.map { |attr| self[attr] }.compact).present?
timestamps.map { |ts| ts.to_time }.max
end
end
diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb
index dcbf38a89f..45313b5e75 100644
--- a/activerecord/lib/active_record/transactions.rb
+++ b/activerecord/lib/active_record/transactions.rb
@@ -220,8 +220,8 @@ module ActiveRecord
# after_commit :do_bar, on: :update
# after_commit :do_baz, on: :destroy
#
- # after_commit :do_foo_bar, :on [:create, :update]
- # after_commit :do_bar_baz, :on [:update, :destroy]
+ # after_commit :do_foo_bar, on: [:create, :update]
+ # after_commit :do_bar_baz, on: [:update, :destroy]
#
# Note that transactional fixtures do not play well with this feature. Please
# use the +test_after_commit+ gem to have these hooks fired in tests.