aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record
diff options
context:
space:
mode:
authorDmitry Polushkin <dmitry.polushkin@gmail.com>2011-12-31 01:10:42 +0000
committerDmitry Polushkin <dmitry.polushkin@gmail.com>2011-12-31 01:10:42 +0000
commit04bc40ff501b1bf81bec7ce3937cb06c896ffc69 (patch)
tree88a33663195900df8a7307aefa2c9aaa561c3973 /activerecord/lib/active_record
parent84eece0a823e9c601ea99a8709f24605a19bcbfd (diff)
parented17983ec56dec689a0311c7f8fcbeba9874e5a4 (diff)
downloadrails-04bc40ff501b1bf81bec7ce3937cb06c896ffc69.tar.gz
rails-04bc40ff501b1bf81bec7ce3937cb06c896ffc69.tar.bz2
rails-04bc40ff501b1bf81bec7ce3937cb06c896ffc69.zip
Merge branch 'master' of git://github.com/rails/rails
Diffstat (limited to 'activerecord/lib/active_record')
-rw-r--r--activerecord/lib/active_record/aggregations.rb7
-rw-r--r--activerecord/lib/active_record/associations.rb34
-rw-r--r--activerecord/lib/active_record/associations/association.rb5
-rw-r--r--activerecord/lib/active_record/associations/association_scope.rb16
-rw-r--r--activerecord/lib/active_record/associations/builder/association.rb12
-rw-r--r--activerecord/lib/active_record/associations/builder/belongs_to.rb6
-rw-r--r--activerecord/lib/active_record/associations/builder/collection_association.rb4
-rw-r--r--activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb2
-rw-r--r--activerecord/lib/active_record/associations/builder/has_many.rb14
-rw-r--r--activerecord/lib/active_record/associations/builder/has_one.rb11
-rw-r--r--activerecord/lib/active_record/associations/builder/singular_association.rb6
-rw-r--r--activerecord/lib/active_record/associations/collection_association.rb29
-rw-r--r--activerecord/lib/active_record/associations/collection_proxy.rb12
-rw-r--r--activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb4
-rw-r--r--activerecord/lib/active_record/associations/has_many_association.rb4
-rw-r--r--activerecord/lib/active_record/associations/has_many_through_association.rb77
-rw-r--r--activerecord/lib/active_record/associations/join_dependency.rb2
-rw-r--r--activerecord/lib/active_record/associations/preloader/association.rb1
-rw-r--r--activerecord/lib/active_record/associations/preloader/through_association.rb1
-rw-r--r--activerecord/lib/active_record/associations/through_association.rb2
-rw-r--r--activerecord/lib/active_record/attribute_assignment.rb221
-rw-r--r--activerecord/lib/active_record/attribute_methods.rb201
-rw-r--r--activerecord/lib/active_record/attribute_methods/dirty.rb8
-rw-r--r--activerecord/lib/active_record/attribute_methods/primary_key.rb90
-rw-r--r--activerecord/lib/active_record/attribute_methods/read.rb155
-rw-r--r--activerecord/lib/active_record/attribute_methods/serialization.rb93
-rw-r--r--activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb27
-rw-r--r--activerecord/lib/active_record/attribute_methods/write.rb32
-rw-r--r--activerecord/lib/active_record/autosave_association.rb24
-rw-r--r--activerecord/lib/active_record/base.rb1845
-rw-r--r--activerecord/lib/active_record/callbacks.rb51
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb294
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb153
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb6
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb66
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb53
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_adapter.rb81
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb94
-rw-r--r--activerecord/lib/active_record/connection_adapters/column.rb20
-rw-r--r--activerecord/lib/active_record/connection_adapters/connection_specification.rb83
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb88
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql_adapter.rb96
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb284
-rw-r--r--activerecord/lib/active_record/connection_adapters/schema_cache.rb50
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb12
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb71
-rw-r--r--activerecord/lib/active_record/connection_handling.rb100
-rw-r--r--activerecord/lib/active_record/core.rb354
-rw-r--r--activerecord/lib/active_record/counter_cache.rb2
-rw-r--r--activerecord/lib/active_record/dynamic_matchers.rb79
-rw-r--r--activerecord/lib/active_record/errors.rb13
-rw-r--r--activerecord/lib/active_record/explain.rb80
-rw-r--r--activerecord/lib/active_record/explain_subscriber.rb21
-rw-r--r--activerecord/lib/active_record/fixtures.rb113
-rw-r--r--activerecord/lib/active_record/identity_map.rb7
-rw-r--r--activerecord/lib/active_record/inheritance.rb182
-rw-r--r--activerecord/lib/active_record/integration.rb49
-rw-r--r--activerecord/lib/active_record/locking/optimistic.rb51
-rw-r--r--activerecord/lib/active_record/log_subscriber.rb6
-rw-r--r--activerecord/lib/active_record/migration.rb61
-rw-r--r--activerecord/lib/active_record/model.rb109
-rw-r--r--activerecord/lib/active_record/model_schema.rb303
-rw-r--r--activerecord/lib/active_record/named_scope.rb200
-rw-r--r--activerecord/lib/active_record/nested_attributes.rb7
-rw-r--r--activerecord/lib/active_record/persistence.rb68
-rw-r--r--activerecord/lib/active_record/query_cache.rb6
-rw-r--r--activerecord/lib/active_record/querying.rb58
-rw-r--r--activerecord/lib/active_record/railtie.rb31
-rw-r--r--activerecord/lib/active_record/railties/databases.rake205
-rw-r--r--activerecord/lib/active_record/readonly_attributes.rb26
-rw-r--r--activerecord/lib/active_record/reflection.rb25
-rw-r--r--activerecord/lib/active_record/relation.rb79
-rw-r--r--activerecord/lib/active_record/relation/calculations.rb16
-rw-r--r--activerecord/lib/active_record/relation/delegation.rb49
-rw-r--r--activerecord/lib/active_record/relation/finder_methods.rb6
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder.rb65
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb30
-rw-r--r--activerecord/lib/active_record/relation/spawn_methods.rb4
-rw-r--r--activerecord/lib/active_record/sanitization.rb194
-rw-r--r--activerecord/lib/active_record/schema_dumper.rb3
-rw-r--r--activerecord/lib/active_record/scoping.rb152
-rw-r--r--activerecord/lib/active_record/scoping/default.rb142
-rw-r--r--activerecord/lib/active_record/scoping/named.rb202
-rw-r--r--activerecord/lib/active_record/session_store.rb4
-rw-r--r--activerecord/lib/active_record/store.rb50
-rw-r--r--activerecord/lib/active_record/test_case.rb20
-rw-r--r--activerecord/lib/active_record/timestamp.rb2
-rw-r--r--activerecord/lib/active_record/transactions.rb12
-rw-r--r--activerecord/lib/active_record/translation.rb22
-rw-r--r--activerecord/lib/active_record/validations/associated.rb5
-rw-r--r--activerecord/lib/active_record/validations/uniqueness.rb4
-rw-r--r--activerecord/lib/active_record/version.rb4
92 files changed, 4447 insertions, 3221 deletions
diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb
index 81ddbba51e..5a8addc4e4 100644
--- a/activerecord/lib/active_record/aggregations.rb
+++ b/activerecord/lib/active_record/aggregations.rb
@@ -172,8 +172,8 @@ module ActiveRecord
# with this option.
# * <tt>:mapping</tt> - Specifies the mapping of entity attributes to attributes of the value
# object. Each mapping is represented as an array where the first item is the name of the
- # entity attribute and the second item is the name the attribute in the value object. The
- # order in which mappings are defined determine the order in which attributes are sent to the
+ # entity attribute and the second item is the name of the attribute in the value object. The
+ # order in which mappings are defined determines the order in which attributes are sent to the
# value class constructor.
# * <tt>:allow_nil</tt> - Specifies that the value object will not be instantiated when all mapped
# attributes are +nil+. Setting the value object to +nil+ has the effect of writing +nil+ to all
@@ -191,7 +191,8 @@ module ActiveRecord
#
# Option examples:
# composed_of :temperature, :mapping => %w(reading celsius)
- # composed_of :balance, :class_name => "Money", :mapping => %w(balance amount), :converter => Proc.new { |balance| balance.to_money }
+ # composed_of :balance, :class_name => "Money", :mapping => %w(balance amount),
+ # :converter => Proc.new { |balance| balance.to_money }
# composed_of :address, :mapping => [ %w(address_street street), %w(address_city city) ]
# composed_of :gps_location
# composed_of :gps_location, :allow_nil => true
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index 0952ea2829..0efa111d12 100644
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -70,7 +70,7 @@ module ActiveRecord
end
end
- class HasManyThroughNestedAssociationsAreReadonly < ActiveRecordError #:nodoc
+ class HasManyThroughNestedAssociationsAreReadonly < ActiveRecordError #:nodoc:
def initialize(owner, reflection)
super("Cannot modify association '#{owner.class.name}##{reflection.name}' because it goes through more than one other association.")
end
@@ -196,6 +196,26 @@ module ActiveRecord
# * <tt>Project#categories.empty?, Project#categories.size, Project#categories, Project#categories<<(category1),</tt>
# <tt>Project#categories.delete(category1)</tt>
#
+ # === Overriding generated methods
+ #
+ # Association methods are generated in a module that is included into the model class,
+ # which allows you to easily override with your own methods and call the original
+ # generated method with +super+. For example:
+ #
+ # class Car < ActiveRecord::Base
+ # belongs_to :owner
+ # belongs_to :old_owner
+ # def owner=(new_owner)
+ # self.old_owner = self.owner
+ # super
+ # end
+ # end
+ #
+ # If your model class is <tt>Project</tt>, the module is
+ # named <tt>Project::GeneratedFeatureMethods</tt>. The GeneratedFeatureMethods module is
+ # included in the model class immediately after the (anonymous) generated attributes methods
+ # module, meaning an association will override the methods for an attribute with the same name.
+ #
# === A word of warning
#
# Don't create associations that have the same name as instance methods of
@@ -1165,7 +1185,7 @@ module ActiveRecord
# has_many :subscribers, :through => :subscriptions, :source => :user
# has_many :subscribers, :class_name => "Person", :finder_sql => Proc.new {
# %Q{
- # SELECT DISTINCT people.*
+ # SELECT DISTINCT *
# FROM people p, post_subscriptions ps
# WHERE ps.post_id = #{id} AND ps.person_id = p.id
# ORDER BY p.first_name
@@ -1424,18 +1444,18 @@ module ActiveRecord
# join table with a migration such as this:
#
# class CreateDevelopersProjectsJoinTable < ActiveRecord::Migration
- # def self.up
+ # def change
# create_table :developers_projects, :id => false do |t|
# t.integer :developer_id
# t.integer :project_id
# end
# end
- #
- # def self.down
- # drop_table :developers_projects
- # end
# end
#
+ # It's also a good idea to add indexes to each of those columns to speed up the joins process.
+ # However, in MySQL it is advised to add a compound index for both of the columns as MySQL only
+ # uses one index per table during the lookup.
+ #
# Adds the following methods for retrieval and query:
#
# [collection(force_reload = false)]
diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb
index d1e3ff8e38..7887d59aad 100644
--- a/activerecord/lib/active_record/associations/association.rb
+++ b/activerecord/lib/active_record/associations/association.rb
@@ -231,10 +231,7 @@ module ActiveRecord
def build_record(attributes, options)
reflection.build_association(attributes, options) do |record|
- record.assign_attributes(
- create_scope.except(*record.changed),
- :without_protection => true
- )
+ record.assign_attributes(create_scope.except(*record.changed), :without_protection => true)
end
end
end
diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb
index 6cc401e6cc..090f690f0d 100644
--- a/activerecord/lib/active_record/associations/association_scope.rb
+++ b/activerecord/lib/active_record/associations/association_scope.rb
@@ -20,31 +20,19 @@ module ActiveRecord
# It's okay to just apply all these like this. The options will only be present if the
# association supports that option; this is enforced by the association builder.
scope = scope.apply_finder_options(options.slice(
- :readonly, :include, :order, :limit, :joins, :group, :having, :offset))
+ :readonly, :include, :eager_load, :order, :limit, :joins, :group, :having, :offset, :select))
if options[:through] && !options[:include]
scope = scope.includes(source_options[:include])
end
- if select = select_value
- scope = scope.select(select)
- end
+ scope = scope.uniq if options[:uniq]
add_constraints(scope)
end
private
- def select_value
- select_value = options[:select]
-
- if reflection.collection?
- select_value ||= options[:uniq] && "DISTINCT #{reflection.quoted_table_name}.*"
- end
-
- select_value
- end
-
def add_constraints(scope)
tables = construct_tables
diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb
index 96fca97440..776f0d0469 100644
--- a/activerecord/lib/active_record/associations/builder/association.rb
+++ b/activerecord/lib/active_record/associations/builder/association.rb
@@ -1,7 +1,7 @@
module ActiveRecord::Associations::Builder
class Association #:nodoc:
class_attribute :valid_options
- self.valid_options = [:class_name, :foreign_key, :select, :conditions, :include, :extend, :readonly, :validate]
+ self.valid_options = [:class_name, :foreign_key, :select, :conditions, :include, :eager_load, :extend, :readonly, :validate]
# Set by subclasses
class_attribute :macro
@@ -16,6 +16,10 @@ module ActiveRecord::Associations::Builder
@model, @name, @options = model, name, options
end
+ def mixin
+ @model.generated_feature_methods
+ end
+
def build
validate_options
reflection = model.create_reflection(self.class.macro, name, options, model)
@@ -36,16 +40,14 @@ module ActiveRecord::Associations::Builder
def define_readers
name = self.name
-
- model.redefine_method(name) do |*params|
+ mixin.redefine_method(name) do |*params|
association(name).reader(*params)
end
end
def define_writers
name = self.name
-
- model.redefine_method("#{name}=") do |value|
+ mixin.redefine_method("#{name}=") do |value|
association(name).writer(value)
end
end
diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb
index f6d26840c2..1759a41d93 100644
--- a/activerecord/lib/active_record/associations/builder/belongs_to.rb
+++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb
@@ -25,14 +25,14 @@ module ActiveRecord::Associations::Builder
name = self.name
method_name = "belongs_to_counter_cache_after_create_for_#{name}"
- model.redefine_method(method_name) do
+ mixin.redefine_method(method_name) do
record = send(name)
record.class.increment_counter(cache_column, record.id) unless record.nil?
end
model.after_create(method_name)
method_name = "belongs_to_counter_cache_before_destroy_for_#{name}"
- model.redefine_method(method_name) do
+ mixin.redefine_method(method_name) do
record = send(name)
record.class.decrement_counter(cache_column, record.id) unless record.nil?
end
@@ -48,7 +48,7 @@ module ActiveRecord::Associations::Builder
method_name = "belongs_to_touch_after_save_or_destroy_for_#{name}"
touch = options[:touch]
- model.redefine_method(method_name) do
+ mixin.redefine_method(method_name) do
record = send(name)
unless record.nil?
diff --git a/activerecord/lib/active_record/associations/builder/collection_association.rb b/activerecord/lib/active_record/associations/builder/collection_association.rb
index f62209a226..35f9a3ae8e 100644
--- a/activerecord/lib/active_record/associations/builder/collection_association.rb
+++ b/activerecord/lib/active_record/associations/builder/collection_association.rb
@@ -58,7 +58,7 @@ module ActiveRecord::Associations::Builder
super
name = self.name
- model.redefine_method("#{name.to_s.singularize}_ids") do
+ mixin.redefine_method("#{name.to_s.singularize}_ids") do
association(name).ids_reader
end
end
@@ -67,7 +67,7 @@ module ActiveRecord::Associations::Builder
super
name = self.name
- model.redefine_method("#{name.to_s.singularize}_ids=") do |ids|
+ mixin.redefine_method("#{name.to_s.singularize}_ids=") do |ids|
association(name).ids_writer(ids)
end
end
diff --git a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb
index 30fc44b4c2..0b634ab944 100644
--- a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb
+++ b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb
@@ -18,7 +18,7 @@ module ActiveRecord::Associations::Builder
model.send(:include, Module.new {
class_eval <<-RUBY, __FILE__, __LINE__ + 1
def destroy_associations
- association(#{name.to_sym.inspect}).delete_all
+ association(#{name.to_sym.inspect}).delete_all_on_destroy
super
end
RUBY
diff --git a/activerecord/lib/active_record/associations/builder/has_many.rb b/activerecord/lib/active_record/associations/builder/has_many.rb
index ecbc70888f..9c24f40690 100644
--- a/activerecord/lib/active_record/associations/builder/has_many.rb
+++ b/activerecord/lib/active_record/associations/builder/has_many.rb
@@ -28,7 +28,7 @@ module ActiveRecord::Associations::Builder
def define_destroy_dependency_method
name = self.name
- model.send(:define_method, dependency_method_name) do
+ mixin.redefine_method(dependency_method_name) do
send(name).each do |o|
# No point in executing the counter update since we're going to destroy the parent anyway
counter_method = ('belongs_to_counter_cache_before_destroy_for_' + self.class.name.downcase).to_sym
@@ -45,15 +45,21 @@ module ActiveRecord::Associations::Builder
def define_delete_all_dependency_method
name = self.name
- model.send(:define_method, dependency_method_name) do
+ mixin.redefine_method(dependency_method_name) do
+ association(name).delete_all_on_destroy
+ end
+ end
+
+ def define_nullify_dependency_method
+ name = self.name
+ mixin.redefine_method(dependency_method_name) do
send(name).delete_all
end
end
- alias :define_nullify_dependency_method :define_delete_all_dependency_method
def define_restrict_dependency_method
name = self.name
- model.send(:define_method, dependency_method_name) do
+ mixin.redefine_method(dependency_method_name) do
raise ActiveRecord::DeleteRestrictionError.new(name) unless send(name).empty?
end
end
diff --git a/activerecord/lib/active_record/associations/builder/has_one.rb b/activerecord/lib/active_record/associations/builder/has_one.rb
index 88c0d3e90f..7a6cd3890f 100644
--- a/activerecord/lib/active_record/associations/builder/has_one.rb
+++ b/activerecord/lib/active_record/associations/builder/has_one.rb
@@ -44,18 +44,17 @@ module ActiveRecord::Associations::Builder
end
def define_destroy_dependency_method
- model.send(:class_eval, <<-eoruby, __FILE__, __LINE__ + 1)
- def #{dependency_method_name}
- association(#{name.to_sym.inspect}).delete
- end
- eoruby
+ name = self.name
+ mixin.redefine_method(dependency_method_name) do
+ association(name).delete
+ end
end
alias :define_delete_dependency_method :define_destroy_dependency_method
alias :define_nullify_dependency_method :define_destroy_dependency_method
def define_restrict_dependency_method
name = self.name
- model.redefine_method(dependency_method_name) do
+ mixin.redefine_method(dependency_method_name) do
raise ActiveRecord::DeleteRestrictionError.new(name) unless send(name).nil?
end
end
diff --git a/activerecord/lib/active_record/associations/builder/singular_association.rb b/activerecord/lib/active_record/associations/builder/singular_association.rb
index 0cbbba041a..436b6c1524 100644
--- a/activerecord/lib/active_record/associations/builder/singular_association.rb
+++ b/activerecord/lib/active_record/associations/builder/singular_association.rb
@@ -16,15 +16,15 @@ module ActiveRecord::Associations::Builder
def define_constructors
name = self.name
- model.redefine_method("build_#{name}") do |*params, &block|
+ mixin.redefine_method("build_#{name}") do |*params, &block|
association(name).build(*params, &block)
end
- model.redefine_method("create_#{name}") do |*params, &block|
+ mixin.redefine_method("create_#{name}") do |*params, &block|
association(name).create(*params, &block)
end
- model.redefine_method("create_#{name}!") do |*params, &block|
+ mixin.redefine_method("create_#{name}!") do |*params, &block|
association(name).create!(*params, &block)
end
end
diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb
index cec876149c..fe9f30bd2a 100644
--- a/activerecord/lib/active_record/associations/collection_association.rb
+++ b/activerecord/lib/active_record/associations/collection_association.rb
@@ -49,10 +49,18 @@ module ActiveRecord
end
else
column = "#{reflection.quoted_table_name}.#{reflection.association_primary_key}"
+ relation = scoped
- scoped.select(column).map! do |record|
- record.send(reflection.association_primary_key)
+ including = (relation.eager_load_values + relation.includes_values).uniq
+
+ if including.any?
+ join_dependency = ActiveRecord::Associations::JoinDependency.new(reflection.klass, including, [])
+ relation = join_dependency.join_associations.inject(relation) do |r, association|
+ association.join_relation(r)
+ end
end
+
+ relation.uniq.pluck(column)
end
end
@@ -152,6 +160,13 @@ module ActiveRecord
end
end
+ # Called when the association is declared as :dependent => :delete_all. This is
+ # an optimised version which avoids loading the records into memory. Not really
+ # for public consumption.
+ def delete_all_on_destroy
+ scoped.delete_all
+ end
+
# Destroy all the records from this association.
#
# See destroy for more info.
@@ -235,7 +250,7 @@ module ActiveRecord
# This method is abstract in the sense that it relies on
# +count_records+, which is a method descendants have to provide.
def size
- if owner.new_record? || (loaded? && !options[:uniq])
+ if !find_target? || (loaded? && !options[:uniq])
target.size
elsif !loaded? && options[:group]
load_target.size
@@ -344,8 +359,12 @@ module ActiveRecord
if options[:counter_sql]
interpolate(options[:counter_sql])
else
- # replace the SELECT clause with COUNT(*), preserving any hints within /* ... */
- interpolate(options[:finder_sql]).sub(/SELECT\b(\/\*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" }
+ # replace the SELECT clause with COUNT(SELECTS), preserving any hints within /* ... */
+ interpolate(options[:finder_sql]).sub(/SELECT\b(\/\*.*?\*\/ )?(.*)\bFROM\b/im) do
+ count_with = $2.to_s
+ count_with = '*' if count_with.blank? || count_with =~ /,/
+ "SELECT #{$1}COUNT(#{count_with}) FROM"
+ end
end
end
diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb
index 3181ca9a32..eb320bc774 100644
--- a/activerecord/lib/active_record/associations/collection_proxy.rb
+++ b/activerecord/lib/active_record/associations/collection_proxy.rb
@@ -39,10 +39,9 @@ module ActiveRecord
instance_methods.each { |m| undef_method m unless m.to_s =~ /^(?:nil\?|send|object_id|to_a)$|^__|^respond_to|proxy_/ }
delegate :group, :order, :limit, :joins, :where, :preload, :eager_load, :includes, :from,
- :lock, :readonly, :having, :to => :scoped
+ :lock, :readonly, :having, :pluck, :to => :scoped
- delegate :target, :load_target, :loaded?, :scoped,
- :to => :@association
+ delegate :target, :load_target, :loaded?, :to => :@association
delegate :select, :find, :first, :last,
:build, :create, :create!,
@@ -62,6 +61,13 @@ module ActiveRecord
@association
end
+ def scoped
+ association = @association
+ association.scoped.extending do
+ define_method(:proxy_association) { association }
+ end
+ end
+
def respond_to?(name, include_private = false)
super ||
(load_target && target.respond_to?(name, include_private)) ||
diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb
index 1f917f58f2..a4cea99372 100644
--- a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb
+++ b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb
@@ -32,6 +32,10 @@ module ActiveRecord
record
end
+ # ActiveRecord::Relation#delete_all needs to support joins before we can use a
+ # SQL-only implementation.
+ alias delete_all_on_destroy delete_all
+
private
def count_records
diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb
index 50ee60284c..059e6c77bc 100644
--- a/activerecord/lib/active_record/associations/has_many_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_association.rb
@@ -99,6 +99,10 @@ module ActiveRecord
end
end
end
+
+ def foreign_key_present?
+ owner.attribute_present?(reflection.association_primary_key)
+ end
end
end
end
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 2e818dca5d..9657cb081d 100644
--- a/activerecord/lib/active_record/associations/has_many_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_through_association.rb
@@ -6,6 +6,13 @@ module ActiveRecord
class HasManyThroughAssociation < HasManyAssociation #:nodoc:
include ThroughAssociation
+ def initialize(owner, reflection)
+ super
+
+ @through_records = {}
+ @through_association = nil
+ end
+
# Returns the size of the collection by executing a SELECT COUNT(*) query if the collection hasn't been
# loaded and calling collection.size if it has. If it's more likely than not that the collection does
# have a size larger than zero, and you need to fetch that collection afterwards, it'll take one fewer
@@ -42,27 +49,40 @@ module ActiveRecord
end
end
- through_record(record).save!
+ save_through_record(record)
update_counter(1)
record
end
- private
+ # ActiveRecord::Relation#delete_all needs to support joins before we can use a
+ # SQL-only implementation.
+ alias delete_all_on_destroy delete_all
- def through_record(record)
- through_association = owner.association(through_reflection.name)
- attributes = construct_join_attributes(record)
+ private
- through_record = Array.wrap(through_association.target).find { |candidate|
- candidate.attributes.slice(*attributes.keys) == attributes
- }
+ def through_association
+ @through_association ||= owner.association(through_reflection.name)
+ end
- unless through_record
- through_record = through_association.build(attributes)
+ # We temporarily cache through record that has been build, because if we build a
+ # through record in build_record and then subsequently call insert_record, then we
+ # want to use the exact same object.
+ #
+ # However, after insert_record has been called, we clear the cache entry because
+ # we want it to be possible to have multiple instances of the same record in an
+ # association
+ def build_through_record(record)
+ @through_records[record.object_id] ||= begin
+ through_record = through_association.build(construct_join_attributes(record))
through_record.send("#{source_reflection.name}=", record)
+ through_record
end
+ end
- through_record
+ def save_through_record(record)
+ build_through_record(record).save!
+ ensure
+ @through_records.delete(record.object_id)
end
def build_record(attributes, options = {})
@@ -73,9 +93,9 @@ module ActiveRecord
inverse = source_reflection.inverse_of
if inverse
if inverse.macro == :has_many
- record.send(inverse.name) << through_record(record)
+ record.send(inverse.name) << build_through_record(record)
elsif inverse.macro == :has_one
- record.send("#{inverse.name}=", through_record(record))
+ record.send("#{inverse.name}=", build_through_record(record))
end
end
@@ -104,8 +124,7 @@ module ActiveRecord
def delete_records(records, method)
ensure_not_nested
- through = owner.association(through_reflection.name)
- scope = through.scoped.where(construct_join_attributes(*records))
+ scope = through_association.scoped.where(construct_join_attributes(*records))
case method
when :destroy
@@ -116,7 +135,7 @@ module ActiveRecord
count = scope.delete_all
end
- delete_through_records(through, records)
+ delete_through_records(records)
if through_reflection.macro == :has_many && update_through_counter?(method)
update_counter(-count, through_reflection)
@@ -125,15 +144,25 @@ module ActiveRecord
update_counter(-count)
end
- def delete_through_records(through, records)
- if through_reflection.macro == :has_many
- records.each do |record|
- through.target.delete(through_record(record))
- end
- else
- records.each do |record|
- through.target = nil if through.target == through_record(record)
+ def through_records_for(record)
+ attributes = construct_join_attributes(record)
+ candidates = Array.wrap(through_association.target)
+ candidates.find_all { |c| c.attributes.slice(*attributes.keys) == attributes }
+ end
+
+ def delete_through_records(records)
+ records.each do |record|
+ through_records = through_records_for(record)
+
+ if through_reflection.macro == :has_many
+ through_records.each { |r| through_association.target.delete(r) }
+ else
+ if through_records.include?(through_association.target)
+ through_association.target = nil
+ end
end
+
+ @through_records.delete(record.object_id)
end
end
diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb
index 6c878f0f00..827b01c5ac 100644
--- a/activerecord/lib/active_record/associations/join_dependency.rb
+++ b/activerecord/lib/active_record/associations/join_dependency.rb
@@ -184,7 +184,7 @@ module ActiveRecord
macro = join_part.reflection.macro
if macro == :has_one
- return if record.association_cache.key?(join_part.reflection.name)
+ 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
diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb
index 779f8164cc..298decb0f1 100644
--- a/activerecord/lib/active_record/associations/preloader/association.rb
+++ b/activerecord/lib/active_record/associations/preloader/association.rb
@@ -100,6 +100,7 @@ module ActiveRecord
scope = scope.select(preload_options[:select] || options[:select] || table[Arel.star])
scope = scope.includes(preload_options[:include] || options[:include])
+ scope = scope.eager_load(preload_options[:eager_load] || options[:eager_load])
if options[:as]
scope = scope.where(
diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb
index ad6374d09a..97898c53ae 100644
--- a/activerecord/lib/active_record/associations/preloader/through_association.rb
+++ b/activerecord/lib/active_record/associations/preloader/through_association.rb
@@ -53,6 +53,7 @@ module ActiveRecord
else
if options[:conditions]
through_options[:include] = options[:include] || options[:source]
+ through_options[:eager_load] = options[:eager_load] || options[:source]
through_options[:conditions] = options[:conditions]
end
diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb
index b347a94978..f95e5337c2 100644
--- a/activerecord/lib/active_record/associations/through_association.rb
+++ b/activerecord/lib/active_record/associations/through_association.rb
@@ -44,7 +44,7 @@ module ActiveRecord
join_attributes = {
source_reflection.foreign_key =>
records.map { |record|
- record.send(source_reflection.association_primary_key)
+ record.send(source_reflection.association_primary_key(reflection.klass))
}
}
diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb
new file mode 100644
index 0000000000..bf9fe70b31
--- /dev/null
+++ b/activerecord/lib/active_record/attribute_assignment.rb
@@ -0,0 +1,221 @@
+require 'active_support/concern'
+
+module ActiveRecord
+ module AttributeAssignment
+ extend ActiveSupport::Concern
+ include ActiveModel::MassAssignmentSecurity
+
+ module ClassMethods
+ private
+
+ # The primary key and inheritance column can never be set by mass-assignment for security reasons.
+ def attributes_protected_by_default
+ default = [ primary_key, inheritance_column ]
+ default << 'id' unless primary_key.eql? 'id'
+ default
+ end
+ end
+
+ # Allows you to set all the attributes at once by passing in a hash with keys
+ # matching the attribute names (which again matches the column names).
+ #
+ # If any attributes are protected by either +attr_protected+ or
+ # +attr_accessible+ then only settable attributes will be assigned.
+ #
+ # class User < ActiveRecord::Base
+ # attr_protected :is_admin
+ # end
+ #
+ # user = User.new
+ # user.attributes = { :username => 'Phusion', :is_admin => true }
+ # user.username # => "Phusion"
+ # user.is_admin? # => false
+ def attributes=(new_attributes)
+ return unless new_attributes.is_a?(Hash)
+
+ assign_attributes(new_attributes)
+ end
+
+ # Allows you to set all the attributes for a particular mass-assignment
+ # security role by passing in a hash of attributes with keys matching
+ # the attribute names (which again matches the column names) and the role
+ # name using the :as option.
+ #
+ # To bypass mass-assignment security you can use the :without_protection => true
+ # option.
+ #
+ # class User < ActiveRecord::Base
+ # attr_accessible :name
+ # attr_accessible :name, :is_admin, :as => :admin
+ # end
+ #
+ # user = User.new
+ # user.assign_attributes({ :name => 'Josh', :is_admin => true })
+ # user.name # => "Josh"
+ # user.is_admin? # => false
+ #
+ # user = User.new
+ # user.assign_attributes({ :name => 'Josh', :is_admin => true }, :as => :admin)
+ # user.name # => "Josh"
+ # user.is_admin? # => true
+ #
+ # user = User.new
+ # user.assign_attributes({ :name => 'Josh', :is_admin => true }, :without_protection => true)
+ # user.name # => "Josh"
+ # user.is_admin? # => true
+ def assign_attributes(new_attributes, options = {})
+ return unless new_attributes
+
+ attributes = new_attributes.stringify_keys
+ multi_parameter_attributes = []
+ nested_parameter_attributes = []
+ @mass_assignment_options = options
+
+ unless options[:without_protection]
+ attributes = sanitize_for_mass_assignment(attributes, mass_assignment_role)
+ end
+
+ attributes.each do |k, v|
+ if k.include?("(")
+ multi_parameter_attributes << [ k, v ]
+ elsif respond_to?("#{k}=")
+ if v.is_a?(Hash)
+ nested_parameter_attributes << [ k, v ]
+ else
+ send("#{k}=", v)
+ end
+ else
+ raise(UnknownAttributeError, "unknown attribute: #{k}")
+ end
+ end
+
+ # assign any deferred nested attributes after the base attributes have been set
+ nested_parameter_attributes.each do |k,v|
+ send("#{k}=", v)
+ end
+
+ @mass_assignment_options = nil
+ assign_multiparameter_attributes(multi_parameter_attributes)
+ end
+
+ protected
+
+ def mass_assignment_options
+ @mass_assignment_options ||= {}
+ end
+
+ def mass_assignment_role
+ mass_assignment_options[:as] || :default
+ end
+
+ private
+
+ # Instantiates objects for all attribute classes that needs more than one constructor parameter. This is done
+ # by calling new on the column type or aggregation type (through composed_of) object with these parameters.
+ # So having the pairs written_on(1) = "2004", written_on(2) = "6", written_on(3) = "24", will instantiate
+ # written_on (a date type) with Date.new("2004", "6", "24"). You can also specify a typecast character in the
+ # parentheses to have the parameters typecasted before they're used in the constructor. Use i for Fixnum,
+ # f for Float, s for String, and a for Array. If all the values for a given attribute are empty, the
+ # attribute will be set to nil.
+ def assign_multiparameter_attributes(pairs)
+ execute_callstack_for_multiparameter_attributes(
+ extract_callstack_for_multiparameter_attributes(pairs)
+ )
+ end
+
+ def instantiate_time_object(name, values)
+ if self.class.send(:create_time_zone_conversion_attribute?, name, column_for_attribute(name))
+ Time.zone.local(*values)
+ else
+ Time.time_with_datetime_fallback(self.class.default_timezone, *values)
+ end
+ end
+
+ def execute_callstack_for_multiparameter_attributes(callstack)
+ errors = []
+ callstack.each do |name, values_with_empty_parameters|
+ begin
+ send(name + "=", read_value_from_parameter(name, values_with_empty_parameters))
+ rescue => ex
+ errors << AttributeAssignmentError.new("error on assignment #{values_with_empty_parameters.values.inspect} to #{name}", ex, name)
+ end
+ end
+ unless errors.empty?
+ raise MultiparameterAssignmentErrors.new(errors), "#{errors.size} error(s) on assignment of multiparameter attributes"
+ end
+ end
+
+ def read_value_from_parameter(name, values_hash_from_param)
+ klass = (self.class.reflect_on_aggregation(name.to_sym) || column_for_attribute(name)).klass
+ if values_hash_from_param.values.all?{|v|v.nil?}
+ nil
+ elsif klass == Time
+ read_time_parameter_value(name, values_hash_from_param)
+ elsif klass == Date
+ read_date_parameter_value(name, values_hash_from_param)
+ else
+ read_other_parameter_value(klass, name, values_hash_from_param)
+ end
+ end
+
+ def read_time_parameter_value(name, values_hash_from_param)
+ # If Date bits were not provided, error
+ raise "Missing Parameter" if [1,2,3].any?{|position| !values_hash_from_param.has_key?(position)}
+ max_position = extract_max_param_for_multiparameter_attributes(values_hash_from_param, 6)
+ # If Date bits were provided but blank, then return nil
+ return nil if (1..3).any? {|position| values_hash_from_param[position].blank?}
+
+ set_values = (1..max_position).collect{|position| values_hash_from_param[position] }
+ # If Time bits are not there, then default to 0
+ (3..5).each {|i| set_values[i] = set_values[i].blank? ? 0 : set_values[i]}
+ instantiate_time_object(name, set_values)
+ end
+
+ def read_date_parameter_value(name, values_hash_from_param)
+ return nil if (1..3).any? {|position| values_hash_from_param[position].blank?}
+ set_values = [values_hash_from_param[1], values_hash_from_param[2], values_hash_from_param[3]]
+ begin
+ Date.new(*set_values)
+ rescue ArgumentError # if Date.new raises an exception on an invalid date
+ instantiate_time_object(name, set_values).to_date # we instantiate Time object and convert it back to a date thus using Time's logic in handling invalid dates
+ end
+ end
+
+ def read_other_parameter_value(klass, name, values_hash_from_param)
+ max_position = extract_max_param_for_multiparameter_attributes(values_hash_from_param)
+ values = (1..max_position).collect do |position|
+ raise "Missing Parameter" if !values_hash_from_param.has_key?(position)
+ values_hash_from_param[position]
+ end
+ klass.new(*values)
+ end
+
+ def extract_max_param_for_multiparameter_attributes(values_hash_from_param, upper_cap = 100)
+ [values_hash_from_param.keys.max,upper_cap].min
+ end
+
+ def extract_callstack_for_multiparameter_attributes(pairs)
+ attributes = { }
+
+ pairs.each do |pair|
+ multiparameter_name, value = pair
+ attribute_name = multiparameter_name.split("(").first
+ attributes[attribute_name] = {} unless attributes.include?(attribute_name)
+
+ parameter_value = value.empty? ? nil : type_cast_attribute_value(multiparameter_name, value)
+ attributes[attribute_name][find_parameter_position(multiparameter_name)] ||= parameter_value
+ end
+
+ attributes
+ end
+
+ def type_cast_attribute_value(multiparameter_name, value)
+ multiparameter_name =~ /\([0-9]*([if])\)/ ? value.send("to_" + $1) : value
+ end
+
+ def find_parameter_position(multiparameter_name)
+ multiparameter_name.scan(/\(([0-9]*).*\)/).first.first.to_i
+ end
+
+ end
+end
diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb
index d7bfaa5655..c5834e2fef 100644
--- a/activerecord/lib/active_record/attribute_methods.rb
+++ b/activerecord/lib/active_record/attribute_methods.rb
@@ -7,35 +7,53 @@ module ActiveRecord
extend ActiveSupport::Concern
include ActiveModel::AttributeMethods
+ included do
+ include Read
+ include Write
+ include BeforeTypeCast
+ include Query
+ include PrimaryKey
+ include TimeZoneConversion
+ include Dirty
+ include Serialization
+
+ # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example,
+ # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)).
+ # (Alias for the protected read_attribute method).
+ alias [] read_attribute
+
+ # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+.
+ # (Alias for the protected write_attribute method).
+ alias []= write_attribute
+
+ public :[], :[]=
+ end
+
module ClassMethods
# Generates all the attribute related methods for columns in the database
# accessors, mutators and query methods.
def define_attribute_methods
return if attribute_methods_generated?
-
- if base_class == self
- super(column_names)
- @attribute_methods_generated = true
- else
- base_class.define_attribute_methods
- end
+ superclass.define_attribute_methods unless self == base_class
+ super(column_names)
+ @attribute_methods_generated = true
end
def attribute_methods_generated?
- if base_class == self
- @attribute_methods_generated ||= false
- else
- base_class.attribute_methods_generated?
- end
+ @attribute_methods_generated ||= false
end
- def undefine_attribute_methods(*args)
- if base_class == self
- super
- @attribute_methods_generated = false
- else
- base_class.undefine_attribute_methods(*args)
- end
+ # We will define the methods as instance methods, but will call them as singleton
+ # methods. This allows us to use method_defined? to check if the method exists,
+ # which is fast and won't give any false positives from the ancestors (because
+ # there are no ancestors).
+ def generated_external_attribute_methods
+ @generated_external_attribute_methods ||= Module.new { extend self }
+ end
+
+ def undefine_attribute_methods
+ super if attribute_methods_generated?
+ @attribute_methods_generated = false
end
def instance_method_already_implemented?(method_name)
@@ -43,19 +61,46 @@ module ActiveRecord
raise DangerousAttributeError, "#{method_name} is defined by ActiveRecord"
end
- super
+ if [Base, Model].include?(active_record_super)
+ super
+ else
+ # If B < A and A defines its own attribute method, then we don't want to overwrite that.
+ defined = method_defined_within?(method_name, superclass, superclass.generated_attribute_methods)
+ defined && !ActiveRecord::Base.method_defined?(method_name) || super
+ end
end
# A method name is 'dangerous' if it is already defined by Active Record, but
# not by any ancestors. (So 'puts' is not dangerous but 'save' is.)
- def dangerous_attribute_method?(method_name)
- active_record = ActiveRecord::Base
- superclass = ActiveRecord::Base.superclass
-
- (active_record.method_defined?(method_name) ||
- active_record.private_method_defined?(method_name)) &&
- !superclass.method_defined?(method_name) &&
- !superclass.private_method_defined?(method_name)
+ def dangerous_attribute_method?(name)
+ method_defined_within?(name, Base)
+ end
+
+ def method_defined_within?(name, klass, sup = klass.superclass)
+ if klass.method_defined?(name) || klass.private_method_defined?(name)
+ if sup.method_defined?(name) || sup.private_method_defined?(name)
+ klass.instance_method(name).owner != sup.instance_method(name).owner
+ else
+ true
+ end
+ else
+ false
+ end
+ end
+
+ def attribute_method?(attribute)
+ super || (table_exists? && column_names.include?(attribute.to_s.sub(/=$/, '')))
+ end
+
+ # Returns an array of column names as strings if it's not
+ # an abstract class and table exists.
+ # Otherwise it returns an empty array.
+ def attribute_names
+ @attribute_names ||= if !abstract_class? && table_exists?
+ column_names
+ else
+ []
+ end
end
end
@@ -94,9 +139,105 @@ module ActiveRecord
super
end
+ # Returns true if the given attribute is in the attributes hash
+ def has_attribute?(attr_name)
+ @attributes.has_key?(attr_name.to_s)
+ end
+
+ # Returns an array of names for the attributes available on this object.
+ def attribute_names
+ @attributes.keys
+ end
+
+ # Returns a hash of all the attributes with their names as keys and the values of the attributes as values.
+ def attributes
+ Hash[@attributes.map { |name, _| [name, read_attribute(name)] }]
+ end
+
+ # 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.
+ #
+ # person = Person.create!(:name => "David Heinemeier Hansson " * 3)
+ #
+ # person.attribute_for_inspect(:name)
+ # # => '"David Heinemeier Hansson David Heinemeier Hansson D..."'
+ #
+ # person.attribute_for_inspect(:created_at)
+ # # => '"2009-01-12 04:48:57"'
+ def attribute_for_inspect(attr_name)
+ value = read_attribute(attr_name)
+
+ if value.is_a?(String) && value.length > 50
+ "#{value[0..50]}...".inspect
+ elsif value.is_a?(Date) || value.is_a?(Time)
+ %("#{value.to_s(:db)}")
+ else
+ value.inspect
+ end
+ end
+
+ # Returns true if the specified +attribute+ has been set by the user or by a database load and is neither
+ # nil nor empty? (the latter only applies to objects that respond to empty?, most notably Strings).
+ def attribute_present?(attribute)
+ value = read_attribute(attribute)
+ !value.nil? || (value.respond_to?(:empty?) && !value.empty?)
+ end
+
+ # Returns the column object for the named attribute.
+ def column_for_attribute(name)
+ self.class.columns_hash[name.to_s]
+ end
+
protected
- def attribute_method?(attr_name)
- attr_name == 'id' || (defined?(@attributes) && @attributes.include?(attr_name))
+
+ def clone_attributes(reader_method = :read_attribute, attributes = {})
+ attribute_names.each do |name|
+ attributes[name] = clone_attribute_value(reader_method, name)
end
+ attributes
+ end
+
+ def clone_attribute_value(reader_method, attribute_name)
+ value = send(reader_method, attribute_name)
+ value.duplicable? ? value.clone : value
+ rescue TypeError, NoMethodError
+ value
+ end
+
+ # Returns a copy of the attributes hash where all the values have been safely quoted for use in
+ # an Arel insert/update method.
+ def arel_attributes_values(include_primary_key = true, include_readonly_attributes = true, attribute_names = @attributes.keys)
+ attrs = {}
+ klass = self.class
+ arel_table = klass.arel_table
+
+ attribute_names.each do |name|
+ if (column = column_for_attribute(name)) && (include_primary_key || !column.primary)
+
+ if include_readonly_attributes || !self.class.readonly_attributes.include?(name)
+
+ value = if klass.serialized_attributes.include?(name)
+ @attributes[name].serialized_value
+ else
+ # FIXME: we need @attributes to be used consistently.
+ # If the values stored in @attributes were already type
+ # casted, this code could be simplified
+ read_attribute(name)
+ end
+
+ attrs[arel_table[name]] = value
+ end
+ end
+ end
+
+ attrs
+ end
+
+ def attribute_method?(attr_name)
+ attr_name == 'id' || (defined?(@attributes) && @attributes.include?(attr_name))
+ end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb
index 3eff3d54e3..40e4a97e73 100644
--- a/activerecord/lib/active_record/attribute_methods/dirty.rb
+++ b/activerecord/lib/active_record/attribute_methods/dirty.rb
@@ -13,7 +13,7 @@ module ActiveRecord
raise "You cannot include Dirty after Timestamp"
end
- class_attribute :partial_updates
+ config_attribute :partial_updates
self.partial_updates = true
end
@@ -34,9 +34,9 @@ module ActiveRecord
@previously_changed = changes
@changed_attributes.clear
end
- rescue
- IdentityMap.remove(self) if IdentityMap.enabled?
- raise
+ rescue
+ IdentityMap.remove(self) if IdentityMap.enabled?
+ raise
end
# <tt>reload</tt> the record and clears changed attributes.
diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb
index 36d7f4ad11..a7785f8786 100644
--- a/activerecord/lib/active_record/attribute_methods/primary_key.rb
+++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb
@@ -5,16 +5,48 @@ module ActiveRecord
# Returns this record's primary key value wrapped in an Array if one is available
def to_key
- key = send(self.class.primary_key)
+ key = self.id
[key] if key
end
+ # Returns the primary key value
+ def id
+ read_attribute(self.class.primary_key)
+ end
+
+ # Sets the primary key value
+ def id=(value)
+ write_attribute(self.class.primary_key, value)
+ end
+
+ # Queries the primary key value
+ def id?
+ query_attribute(self.class.primary_key)
+ end
+
module ClassMethods
+ def define_method_attribute(attr_name)
+ super
+
+ if attr_name == primary_key && attr_name != 'id'
+ generated_attribute_methods.send(:alias_method, :id, primary_key)
+ generated_external_attribute_methods.module_eval <<-CODE, __FILE__, __LINE__
+ def id(v, attributes, attributes_cache, attr_name)
+ attr_name = '#{primary_key}'
+ send(attr_name, attributes[attr_name], attributes, attributes_cache, attr_name)
+ end
+ CODE
+ end
+ end
+
+ def dangerous_attribute_method?(method_name)
+ super && !['id', 'id=', 'id?'].include?(method_name)
+ end
+
# Defines the primary key field -- can be overridden in subclasses. Overwriting will negate any effect of the
# primary_key_prefix_type setting, though.
def primary_key
- @primary_key ||= reset_primary_key
- raise ActiveRecord::UnknownPrimaryKey.new(self) unless @primary_key
+ @primary_key = reset_primary_key unless defined? @primary_key
@primary_key
end
@@ -24,16 +56,11 @@ module ActiveRecord
end
def reset_primary_key #:nodoc:
- key = self == base_class ? get_primary_key(base_class.name) :
- base_class.primary_key
-
- set_primary_key(key)
- key
- end
-
- def primary_key? #:nodoc:
- @primary_key ||= reset_primary_key
- !@primary_key.nil?
+ if self == base_class
+ self.primary_key = get_primary_key(base_class.name)
+ else
+ self.primary_key = base_class.primary_key
+ end
end
def get_primary_key(base_name) #:nodoc:
@@ -45,35 +72,32 @@ module ActiveRecord
when :table_name_with_underscore
base_name.foreign_key
else
- if ActiveRecord::Base != self && connection.table_exists?(table_name)
- connection.primary_key(table_name)
+ if ActiveRecord::Base != self && table_exists?
+ connection.schema_cache.primary_keys[table_name]
else
'id'
end
end
end
- attr_accessor :original_primary_key
-
- # Attribute writer for the primary key column
- def primary_key=(value)
- @quoted_primary_key = nil
- @primary_key = value
- end
-
- # Sets the name of the primary key column to use to the given value,
- # or (if the value is nil or false) to the value returned by the given
- # block.
+ # Sets the name of the primary key column.
+ #
+ # class Project < ActiveRecord::Base
+ # self.primary_key = "sysid"
+ # end
+ #
+ # You can also define the primary_key method yourself:
#
# class Project < ActiveRecord::Base
- # set_primary_key "sysid"
+ # def self.primary_key
+ # "foo_" + super
+ # end
# end
- def set_primary_key(value = nil, &block)
- @quoted_primary_key = nil
- @primary_key ||= ''
- self.original_primary_key = @primary_key
- value &&= value.to_s
- self.primary_key = block_given? ? instance_eval(&block) : value
+ # Project.primary_key # => "foo_id"
+ def primary_key=(value)
+ @original_primary_key = @primary_key if defined?(@primary_key)
+ @primary_key = value && value.to_s
+ @quoted_primary_key = nil
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb
index 8566ecad14..964c4123ef 100644
--- a/activerecord/lib/active_record/attribute_methods/read.rb
+++ b/activerecord/lib/active_record/attribute_methods/read.rb
@@ -6,11 +6,8 @@ module ActiveRecord
ATTRIBUTE_TYPES_CACHED_BY_DEFAULT = [:datetime, :timestamp, :time, :date]
included do
- cattr_accessor :attribute_types_cached_by_default, :instance_writer => false
+ config_attribute :attribute_types_cached_by_default, :global => true
self.attribute_types_cached_by_default = ATTRIBUTE_TYPES_CACHED_BY_DEFAULT
-
- # Undefine id so it can be used as an attribute name
- undef_method(:id) if method_defined?(:id)
end
module ClassMethods
@@ -32,107 +29,101 @@ module ActiveRecord
cached_attributes.include?(attr_name)
end
+ def undefine_attribute_methods
+ generated_external_attribute_methods.module_eval do
+ instance_methods.each { |m| undef_method(m) }
+ end
+
+ super
+ end
+
+ def type_cast_attribute(attr_name, attributes, cache = {}) #:nodoc:
+ return unless attr_name
+ attr_name = attr_name.to_s
+
+ if generated_external_attribute_methods.method_defined?(attr_name)
+ if attributes.has_key?(attr_name) || attr_name == 'id'
+ generated_external_attribute_methods.send(attr_name, attributes[attr_name], attributes, cache, attr_name)
+ end
+ elsif !attribute_methods_generated?
+ # If we haven't generated the caster methods yet, do that and
+ # then try again
+ define_attribute_methods
+ type_cast_attribute(attr_name, attributes, cache)
+ else
+ # If we get here, the attribute has no associated DB column, so
+ # just return it verbatim.
+ attributes[attr_name]
+ end
+ end
+
protected
+ # We want to generate the methods via module_eval rather than define_method,
+ # because define_method is slower on dispatch and uses more memory (because it
+ # creates a closure).
+ #
+ # But sometimes the database might return columns with characters that are not
+ # allowed in normal method names (like 'my_column(omg)'. So to work around this
+ # we first define with the __temp__ identifier, and then use alias method to
+ # rename it to what we want.
def define_method_attribute(attr_name)
- if serialized_attributes.include?(attr_name)
- define_read_method_for_serialized_attribute(attr_name)
- else
- define_read_method(attr_name, attr_name, columns_hash[attr_name])
- end
+ cast_code = attribute_cast_code(attr_name)
- if primary_key? && attr_name == primary_key && attr_name != "id"
- define_read_method('id', attr_name, columns_hash[attr_name])
- end
+ generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
+ def __temp__
+ #{internal_attribute_access_code(attr_name, cast_code)}
+ end
+ alias_method '#{attr_name}', :__temp__
+ undef_method :__temp__
+ STR
+
+ generated_external_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
+ def __temp__(v, attributes, attributes_cache, attr_name)
+ #{external_attribute_access_code(attr_name, cast_code)}
+ end
+ alias_method '#{attr_name}', :__temp__
+ undef_method :__temp__
+ STR
end
private
def cacheable_column?(column)
- serialized_attributes.include?(column.name) || attribute_types_cached_by_default.include?(column.type)
- end
-
- # Define read method for serialized attribute.
- def define_read_method_for_serialized_attribute(attr_name)
- access_code = "@attributes_cache['#{attr_name}'] ||= @attributes['#{attr_name}']"
- generated_attribute_methods.module_eval("def _#{attr_name}; #{access_code}; end; alias #{attr_name} _#{attr_name}", __FILE__, __LINE__)
+ attribute_types_cached_by_default.include?(column.type)
end
- # Define an attribute reader method. Cope with nil column.
- # method_name is the same as attr_name except when a non-standard primary key is used,
- # we still define #id as an accessor for the key
- def define_read_method(method_name, attr_name, column)
- cast_code = column.type_cast_code('v')
- access_code = "(v=@attributes['#{attr_name}']) && #{cast_code}"
+ def internal_attribute_access_code(attr_name, cast_code)
+ access_code = "(v=@attributes[attr_name]) && #{cast_code}"
- unless primary_key? && attr_name.to_s == primary_key.to_s
- access_code.insert(0, "missing_attribute('#{attr_name}', caller) unless @attributes.has_key?('#{attr_name}'); ")
+ unless attr_name == primary_key
+ access_code.insert(0, "missing_attribute(attr_name, caller) unless @attributes.has_key?(attr_name); ")
end
if cache_attribute?(attr_name)
- access_code = "@attributes_cache['#{attr_name}'] ||= (#{access_code})"
+ access_code = "@attributes_cache[attr_name] ||= (#{access_code})"
end
- # Where possible, generate the method by evalling a string, as this will result in
- # faster accesses because it avoids the block eval and then string eval incurred
- # by the second branch.
- #
- # The second, slower, branch is necessary to support instances where the database
- # returns columns with extra stuff in (like 'my_column(omg)').
- if method_name =~ ActiveModel::AttributeMethods::COMPILABLE_REGEXP
- generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__
- def _#{method_name}
- #{access_code}
- end
-
- alias #{method_name} _#{method_name}
- STR
- else
- generated_attribute_methods.module_eval do
- define_method("_#{method_name}") { eval(access_code) }
- alias_method(method_name, "_#{method_name}")
- end
- end
+ "attr_name = '#{attr_name}'; #{access_code}"
end
- end
- # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example,
- # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)).
- def read_attribute(attr_name)
- method = "_#{attr_name}"
- if respond_to? method
- send method if @attributes.has_key?(attr_name.to_s)
- else
- _read_attribute attr_name
- end
- end
+ def external_attribute_access_code(attr_name, cast_code)
+ access_code = "v && #{cast_code}"
- def _read_attribute(attr_name)
- attr_name = attr_name.to_s
- attr_name = self.class.primary_key? && self.class.primary_key if attr_name == 'id'
- value = @attributes[attr_name]
- unless value.nil?
- if column = column_for_attribute(attr_name)
- if unserializable_attribute?(attr_name, column)
- unserialize_attribute(attr_name)
- else
- column.type_cast(value)
+ if cache_attribute?(attr_name)
+ access_code = "attributes_cache[attr_name] ||= (#{access_code})"
end
- else
- value
+
+ access_code
end
- end
- end
- # Returns true if the attribute is of a text column and marked for serialization.
- def unserializable_attribute?(attr_name, column)
- column.text? && self.class.serialized_attributes.include?(attr_name)
+ def attribute_cast_code(attr_name)
+ columns_hash[attr_name].type_cast_code('v')
+ end
end
- # Returns the unserialized object of the attribute.
- def unserialize_attribute(attr_name)
- coder = self.class.serialized_attributes[attr_name]
- unserialized_object = coder.load(@attributes[attr_name])
-
- @attributes.frozen? ? unserialized_object : @attributes[attr_name] = unserialized_object
+ # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example,
+ # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)).
+ def read_attribute(attr_name)
+ self.class.type_cast_attribute(attr_name, @attributes, @attributes_cache)
end
private
diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb
new file mode 100644
index 0000000000..0c8e4e4b9a
--- /dev/null
+++ b/activerecord/lib/active_record/attribute_methods/serialization.rb
@@ -0,0 +1,93 @@
+module ActiveRecord
+ module AttributeMethods
+ module Serialization
+ extend ActiveSupport::Concern
+
+ included do
+ # Returns a hash of all the attributes that have been specified for serialization as
+ # keys and their class restriction as values.
+ config_attribute :serialized_attributes
+ self.serialized_attributes = {}
+ end
+
+ class Attribute < Struct.new(:coder, :value, :state)
+ def unserialized_value
+ state == :serialized ? unserialize : value
+ end
+
+ def serialized_value
+ state == :unserialized ? serialize : value
+ end
+
+ def unserialize
+ self.state = :unserialized
+ self.value = coder.load(value)
+ end
+
+ def serialize
+ self.state = :serialized
+ self.value = coder.dump(value)
+ end
+ end
+
+ module ClassMethods
+ # If you have an attribute that needs to be saved to the database as an object, and retrieved as the same object,
+ # then specify the name of that attribute using this method and it will be handled automatically.
+ # The serialization is done through YAML. If +class_name+ is specified, the serialized object must be of that
+ # class on retrieval or SerializationTypeMismatch will be raised.
+ #
+ # ==== Parameters
+ #
+ # * +attr_name+ - The field name that should be serialized.
+ # * +class_name+ - Optional, class name that the object type should be equal to.
+ #
+ # ==== Example
+ # # Serialize a preferences attribute
+ # class User < ActiveRecord::Base
+ # serialize :preferences
+ # end
+ def serialize(attr_name, class_name = Object)
+ coder = if [:load, :dump].all? { |x| class_name.respond_to?(x) }
+ class_name
+ else
+ Coders::YAMLColumn.new(class_name)
+ end
+
+ # merge new serialized attribute and create new hash to ensure that each class in inheritance hierarchy
+ # has its own hash of own serialized attributes
+ self.serialized_attributes = serialized_attributes.merge(attr_name.to_s => coder)
+ end
+
+ def initialize_attributes(attributes) #:nodoc:
+ super
+
+ serialized_attributes.each do |key, coder|
+ if attributes.key?(key)
+ attributes[key] = Attribute.new(coder, attributes[key], :serialized)
+ end
+ end
+
+ attributes
+ end
+
+ private
+
+ def attribute_cast_code(attr_name)
+ if serialized_attributes.include?(attr_name)
+ "v.unserialized_value"
+ else
+ super
+ end
+ end
+ end
+
+ def type_cast_attribute_for_write(column, value)
+ if column && coder = self.class.serialized_attributes[column.name]
+ Attribute.new(coder, value, :unserialized)
+ else
+ super
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
index 62a3cfa9a5..2f86e32f41 100644
--- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
+++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
@@ -7,30 +7,25 @@ module ActiveRecord
extend ActiveSupport::Concern
included do
- cattr_accessor :time_zone_aware_attributes, :instance_writer => false
+ config_attribute :time_zone_aware_attributes, :global => true
self.time_zone_aware_attributes = false
- class_attribute :skip_time_zone_conversion_for_attributes, :instance_writer => false
+ config_attribute :skip_time_zone_conversion_for_attributes
self.skip_time_zone_conversion_for_attributes = []
end
module ClassMethods
protected
- # Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled.
- # This enhanced read method automatically converts the UTC time stored in the database to the time
+ # The enhanced read method automatically converts the UTC time stored in the database to the time
# zone stored in Time.zone.
- def define_method_attribute(attr_name)
- if create_time_zone_conversion_attribute?(attr_name, columns_hash[attr_name])
- method_body, line = <<-EOV, __LINE__ + 1
- def _#{attr_name}
- cached = @attributes_cache['#{attr_name}']
- return cached if cached
- time = _read_attribute('#{attr_name}')
- @attributes_cache['#{attr_name}'] = time.acts_like?(:time) ? time.in_time_zone : time
- end
- alias #{attr_name} _#{attr_name}
- EOV
- generated_attribute_methods.module_eval(method_body, __FILE__, line)
+ def attribute_cast_code(attr_name)
+ column = columns_hash[attr_name]
+
+ if create_time_zone_conversion_attribute?(attr_name, column)
+ typecast = "v = #{super}"
+ time_zone_conversion = "v.acts_like?(:time) ? v.in_time_zone : v"
+
+ "((#{typecast}) && (#{time_zone_conversion}))"
else
super
end
diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb
index 4db6d71ba6..fde55b95da 100644
--- a/activerecord/lib/active_record/attribute_methods/write.rb
+++ b/activerecord/lib/active_record/attribute_methods/write.rb
@@ -10,17 +10,13 @@ module ActiveRecord
module ClassMethods
protected
def define_method_attribute=(attr_name)
- if attr_name =~ ActiveModel::AttributeMethods::COMPILABLE_REGEXP
+ if attr_name =~ ActiveModel::AttributeMethods::NAME_COMPILABLE_REGEXP
generated_attribute_methods.module_eval("def #{attr_name}=(new_value); write_attribute('#{attr_name}', new_value); end", __FILE__, __LINE__)
else
generated_attribute_methods.send(:define_method, "#{attr_name}=") do |new_value|
write_attribute(attr_name, new_value)
end
end
-
- if primary_key? && attr_name == primary_key && attr_name != "id"
- generated_attribute_methods.module_eval("alias :id= :'#{primary_key}='")
- end
end
end
@@ -32,10 +28,8 @@ module ActiveRecord
@attributes_cache.delete(attr_name)
column = column_for_attribute(attr_name)
- if column && column.number?
- @attributes[attr_name] = convert_number_column_value(value)
- elsif column || @attributes.has_key?(attr_name)
- @attributes[attr_name] = value
+ if column || @attributes.has_key?(attr_name)
+ @attributes[attr_name] = type_cast_attribute_for_write(column, value)
else
raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{attr_name}'"
end
@@ -47,6 +41,26 @@ module ActiveRecord
def attribute=(attribute_name, value)
write_attribute(attribute_name, value)
end
+
+ def type_cast_attribute_for_write(column, value)
+ if column && column.number?
+ convert_number_column_value(value)
+ else
+ value
+ end
+ end
+
+ def convert_number_column_value(value)
+ if value == false
+ 0
+ elsif value == true
+ 1
+ elsif value.is_a?(String) && value.blank?
+ nil
+ else
+ value
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb
index 056170d82a..c86eaba498 100644
--- a/activerecord/lib/active_record/autosave_association.rb
+++ b/activerecord/lib/active_record/autosave_association.rb
@@ -21,6 +21,21 @@ module ActiveRecord
# Note that <tt>:autosave => false</tt> is not same as not declaring <tt>:autosave</tt>.
# When the <tt>:autosave</tt> option is not present new associations are saved.
#
+ # == Validation
+ #
+ # Children records are validated unless <tt>:validate</tt> is +false+.
+ #
+ # == Callbacks
+ #
+ # Association with autosave option defines several callbacks on your
+ # model (before_save, after_create, after_update). Please note that
+ # callbacks are executed in the order they were defined in
+ # model. You should avoid modyfing the association content, before
+ # autosave callbacks are executed. Placing your callbacks after
+ # associations is usually a good practice.
+ #
+ # == Examples
+ #
# === One-to-one Example
#
# class Post
@@ -109,10 +124,7 @@ module ActiveRecord
# Now it _is_ removed from the database:
#
# Comment.find_by_id(id).nil? # => true
- #
- # === Validation
- #
- # Children records are validated unless <tt>:validate</tt> is +false+.
+
module AutosaveAssociation
extend ActiveSupport::Concern
@@ -264,7 +276,7 @@ module ActiveRecord
# turned on for the association.
def validate_single_association(reflection)
association = association_instance_get(reflection.name)
- record = association && association.target
+ record = association && association.reader
association_valid?(reflection, record) if record
end
@@ -331,7 +343,7 @@ module ActiveRecord
if autosave
saved = association.insert_record(record, false)
else
- association.insert_record(record)
+ association.insert_record(record) unless reflection.nested?
end
elsif autosave
saved = record.save(:validate => false)
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index 137b4c6534..d29cf82dad 100644
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -23,9 +23,11 @@ require 'active_support/core_ext/module/delegation'
require 'active_support/core_ext/module/introspection'
require 'active_support/core_ext/object/duplicable'
require 'active_support/core_ext/object/blank'
+require 'active_support/deprecation'
require 'arel'
require 'active_record/errors'
require 'active_record/log_subscriber'
+require 'active_record/explain_subscriber'
module ActiveRecord #:nodoc:
# = Active Record
@@ -115,8 +117,8 @@ module ActiveRecord #:nodoc:
# When joining tables, nested hashes or keys written in the form 'table_name.column_name'
# can be used to qualify the table name of a particular condition. For instance:
#
- # Student.joins(:schools).where(:schools => { :type => 'public' })
- # Student.joins(:schools).where('schools.type' => 'public' )
+ # Student.joins(:schools).where(:schools => { :category => 'public' })
+ # Student.joins(:schools).where('schools.category' => 'public' )
#
# == Overwriting default accessors
#
@@ -325,1843 +327,8 @@ module ActiveRecord #:nodoc:
# So it's possible to assign a logger to the class through <tt>Base.logger=</tt> which will then be used by all
# instances in the current object space.
class Base
- ##
- # :singleton-method:
- # Accepts a logger conforming to the interface of Log4r or the default Ruby 1.8+ Logger class,
- # which is then passed on to any new database connections made and which can be retrieved on both
- # a class and instance level by calling +logger+.
- cattr_accessor :logger, :instance_writer => false
-
- ##
- # :singleton-method:
- # Contains the database configuration - as is typically stored in config/database.yml -
- # as a Hash.
- #
- # For example, the following database.yml...
- #
- # development:
- # adapter: sqlite3
- # database: db/development.sqlite3
- #
- # production:
- # adapter: sqlite3
- # database: db/production.sqlite3
- #
- # ...would result in ActiveRecord::Base.configurations to look like this:
- #
- # {
- # 'development' => {
- # 'adapter' => 'sqlite3',
- # 'database' => 'db/development.sqlite3'
- # },
- # 'production' => {
- # 'adapter' => 'sqlite3',
- # 'database' => 'db/production.sqlite3'
- # }
- # }
- cattr_accessor :configurations, :instance_writer => false
- @@configurations = {}
-
- ##
- # :singleton-method:
- # Accessor for the prefix type that will be prepended to every primary key column name.
- # The options are :table_name and :table_name_with_underscore. If the first is specified,
- # the Product class will look for "productid" instead of "id" as the primary column. If the
- # latter is specified, the Product class will look for "product_id" instead of "id". Remember
- # that this is a global setting for all Active Records.
- cattr_accessor :primary_key_prefix_type, :instance_writer => false
- @@primary_key_prefix_type = nil
-
- ##
- # :singleton-method:
- # Accessor for the name of the prefix string to prepend to every table name. So if set
- # to "basecamp_", all table names will be named like "basecamp_projects", "basecamp_people",
- # etc. This is a convenient way of creating a namespace for tables in a shared database.
- # By default, the prefix is the empty string.
- #
- # If you are organising your models within modules you can add a prefix to the models within
- # a namespace by defining a singleton method in the parent module called table_name_prefix which
- # returns your chosen prefix.
- class_attribute :table_name_prefix, :instance_writer => false
- self.table_name_prefix = ""
-
- ##
- # :singleton-method:
- # Works like +table_name_prefix+, but appends instead of prepends (set to "_basecamp" gives "projects_basecamp",
- # "people_basecamp"). By default, the suffix is the empty string.
- class_attribute :table_name_suffix, :instance_writer => false
- self.table_name_suffix = ""
-
- ##
- # :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.
- class_attribute :pluralize_table_names, :instance_writer => false
- self.pluralize_table_names = true
-
- ##
- # :singleton-method:
- # Determines whether to use Time.local (using :local) or Time.utc (using :utc) when pulling
- # dates and times from the database. This is set to :local by default.
- cattr_accessor :default_timezone, :instance_writer => false
- @@default_timezone = :local
-
- ##
- # :singleton-method:
- # Specifies the format to use when dumping the database schema with Rails'
- # Rakefile. If :sql, the schema is dumped as (potentially database-
- # specific) SQL statements. If :ruby, the schema is dumped as an
- # ActiveRecord::Schema file which can be loaded into any database that
- # supports migrations. Use :ruby if you want to have different database
- # adapters for, e.g., your development and test environments.
- cattr_accessor :schema_format , :instance_writer => false
- @@schema_format = :ruby
-
- ##
- # :singleton-method:
- # Specify whether or not to use timestamps for migration versions
- cattr_accessor :timestamped_migrations , :instance_writer => false
- @@timestamped_migrations = true
-
- # Determine whether to store the full constant name including namespace when using STI
- class_attribute :store_full_sti_class
- self.store_full_sti_class = true
-
- # Stores the default scope for the class
- class_attribute :default_scopes, :instance_writer => false
- self.default_scopes = []
-
- # Returns a hash of all the attributes that have been specified for serialization as
- # keys and their class restriction as values.
- class_attribute :serialized_attributes
- self.serialized_attributes = {}
-
- class_attribute :_attr_readonly, :instance_writer => false
- self._attr_readonly = []
-
- class << self # Class methods
- delegate :find, :first, :first!, :last, :last!, :all, :exists?, :any?, :many?, :to => :scoped
- delegate :first_or_create, :first_or_create!, :first_or_initialize, :to => :scoped
- delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, :to => :scoped
- delegate :find_each, :find_in_batches, :to => :scoped
- delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins, :where, :preload, :eager_load, :includes, :from, :lock, :readonly, :having, :create_with, :to => :scoped
- delegate :count, :average, :minimum, :maximum, :sum, :calculate, :to => :scoped
-
- # Executes a custom SQL query against your database and returns all the results. The results will
- # be returned as an array with columns requested encapsulated as attributes of the model you call
- # this method from. If you call <tt>Product.find_by_sql</tt> then the results will be returned in
- # a Product object with the attributes you specified in the SQL query.
- #
- # If you call a complicated SQL query which spans multiple tables the columns specified by the
- # SELECT will be attributes of the model, whether or not they are columns of the corresponding
- # table.
- #
- # The +sql+ parameter is a full SQL query as a string. It will be called as is, there will be
- # no database agnostic conversions performed. This should be a last resort because using, for example,
- # MySQL specific terms will lock you to using that particular database engine or require you to
- # change your call if you switch engines.
- #
- # ==== Examples
- # # A simple SQL query spanning multiple tables
- # Post.find_by_sql "SELECT p.title, c.author FROM posts p, comments c WHERE p.id = c.post_id"
- # > [#<Post:0x36bff9c @attributes={"title"=>"Ruby Meetup", "first_name"=>"Quentin"}>, ...]
- #
- # # You can use the same string replacement techniques as you can with ActiveRecord#find
- # Post.find_by_sql ["SELECT title FROM posts WHERE author = ? AND created > ?", author_id, start_date]
- # > [#<Post:0x36bff9c @attributes={"title"=>"The Cheap Man Buys Twice"}>, ...]
- def find_by_sql(sql, binds = [])
- connection.select_all(sanitize_sql(sql), "#{name} Load", binds).collect! { |record| instantiate(record) }
- end
-
- # Creates an object (or multiple objects) and saves it to the database, if validations pass.
- # The resulting object is returned whether the object was saved successfully to the database or not.
- #
- # The +attributes+ parameter can be either be a Hash or an Array of Hashes. These Hashes describe the
- # attributes on the objects that are to be created.
- #
- # +create+ respects mass-assignment security and accepts either +:as+ or +:without_protection+ options
- # in the +options+ parameter.
- #
- # ==== Examples
- # # Create a single new object
- # User.create(:first_name => 'Jamie')
- #
- # # Create a single new object using the :admin mass-assignment security role
- # User.create({ :first_name => 'Jamie', :is_admin => true }, :as => :admin)
- #
- # # Create a single new object bypassing mass-assignment security
- # User.create({ :first_name => 'Jamie', :is_admin => true }, :without_protection => true)
- #
- # # Create an Array of new objects
- # User.create([{ :first_name => 'Jamie' }, { :first_name => 'Jeremy' }])
- #
- # # Create a single object and pass it into a block to set other attributes.
- # User.create(:first_name => 'Jamie') do |u|
- # u.is_admin = false
- # end
- #
- # # Creating an Array of new objects using a block, where the block is executed for each object:
- # User.create([{ :first_name => 'Jamie' }, { :first_name => 'Jeremy' }]) do |u|
- # u.is_admin = false
- # end
- def create(attributes = nil, options = {}, &block)
- if attributes.is_a?(Array)
- attributes.collect { |attr| create(attr, options, &block) }
- else
- object = new(attributes, options, &block)
- object.save
- object
- end
- end
-
- # Returns the result of an SQL statement that should only include a COUNT(*) in the SELECT part.
- # The use of this method should be restricted to complicated SQL queries that can't be executed
- # using the ActiveRecord::Calculations class methods. Look into those before using this.
- #
- # ==== Parameters
- #
- # * +sql+ - An SQL statement which should return a count query from the database, see the example below.
- #
- # ==== Examples
- #
- # Product.count_by_sql "SELECT COUNT(*) FROM sales s, customers c WHERE s.customer_id = c.id"
- def count_by_sql(sql)
- sql = sanitize_conditions(sql)
- connection.select_value(sql, "#{name} Count").to_i
- end
-
- # Attributes listed as readonly will be used to create a new record but update operations will
- # ignore these fields.
- def attr_readonly(*attributes)
- self._attr_readonly = Set.new(attributes.map { |a| a.to_s }) + (self._attr_readonly || [])
- end
-
- # Returns an array of all the attributes that have been specified as readonly.
- def readonly_attributes
- self._attr_readonly
- end
-
- # If you have an attribute that needs to be saved to the database as an object, and retrieved as the same object,
- # then specify the name of that attribute using this method and it will be handled automatically.
- # The serialization is done through YAML. If +class_name+ is specified, the serialized object must be of that
- # class on retrieval or SerializationTypeMismatch will be raised.
- #
- # ==== Parameters
- #
- # * +attr_name+ - The field name that should be serialized.
- # * +class_name+ - Optional, class name that the object type should be equal to.
- #
- # ==== Example
- # # Serialize a preferences attribute
- # class User < ActiveRecord::Base
- # serialize :preferences
- # end
- def serialize(attr_name, class_name = Object)
- coder = if [:load, :dump].all? { |x| class_name.respond_to?(x) }
- class_name
- else
- Coders::YAMLColumn.new(class_name)
- end
-
- # merge new serialized attribute and create new hash to ensure that each class in inheritance hierarchy
- # has its own hash of own serialized attributes
- self.serialized_attributes = serialized_attributes.merge(attr_name.to_s => coder)
- end
-
- # Guesses the table name (in forced lower-case) based on the name of the class in the
- # inheritance hierarchy descending directly from ActiveRecord::Base. So if the hierarchy
- # looks like: Reply < Message < ActiveRecord::Base, then Message is used
- # to guess the table name even when called on Reply. The rules used to do the guess
- # are handled by the Inflector class in Active Support, which knows almost all common
- # English inflections. You can add new inflections in config/initializers/inflections.rb.
- #
- # Nested classes are given table names prefixed by the singular form of
- # the parent's table name. Enclosing modules are not considered.
- #
- # ==== Examples
- #
- # class Invoice < ActiveRecord::Base
- # end
- #
- # file class table_name
- # invoice.rb Invoice invoices
- #
- # class Invoice < ActiveRecord::Base
- # class Lineitem < ActiveRecord::Base
- # end
- # end
- #
- # file class table_name
- # invoice.rb Invoice::Lineitem invoice_lineitems
- #
- # module Invoice
- # class Lineitem < ActiveRecord::Base
- # end
- # end
- #
- # file class table_name
- # invoice/lineitem.rb Invoice::Lineitem lineitems
- #
- # Additionally, the class-level +table_name_prefix+ is prepended and the
- # +table_name_suffix+ is appended. So if you have "myapp_" as a prefix,
- # the table name guess for an Invoice class becomes "myapp_invoices".
- # Invoice::Lineitem becomes "myapp_invoice_lineitems".
- #
- # You can also overwrite this class method to allow for unguessable
- # links, such as a Mouse class with a link to a "mice" table. Example:
- #
- # class Mouse < ActiveRecord::Base
- # set_table_name "mice"
- # end
- def table_name
- reset_table_name
- end
-
- # Returns a quoted version of the table name, used to construct SQL statements.
- def quoted_table_name
- @quoted_table_name ||= connection.quote_table_name(table_name)
- end
-
- # Computes the table name, (re)sets it internally, and returns it.
- def reset_table_name #:nodoc:
- return if abstract_class?
-
- self.table_name = compute_table_name
- end
-
- def full_table_name_prefix #:nodoc:
- (parents.detect{ |p| p.respond_to?(:table_name_prefix) } || self).table_name_prefix
- end
-
- # Defines the column name for use with single table inheritance. Use
- # <tt>set_inheritance_column</tt> to set a different value.
- def inheritance_column
- @inheritance_column ||= "type"
- end
-
- # Lazy-set the sequence name to the connection's default. This method
- # is only ever called once since set_sequence_name overrides it.
- def sequence_name #:nodoc:
- reset_sequence_name
- end
-
- def reset_sequence_name #:nodoc:
- default = connection.default_sequence_name(table_name, primary_key)
- set_sequence_name(default)
- default
- end
-
- # Sets the table name. If the value is nil or false then the value returned by the given
- # block is used.
- #
- # class Project < ActiveRecord::Base
- # set_table_name "project"
- # end
- def set_table_name(value = nil, &block)
- @quoted_table_name = nil
- define_attr_method :table_name, value, &block
- @arel_table = nil
-
- @relation = Relation.new(self, arel_table)
- end
- alias :table_name= :set_table_name
-
- # Sets the name of the inheritance column to use to the given value,
- # or (if the value # is nil or false) to the value returned by the
- # given block.
- #
- # class Project < ActiveRecord::Base
- # set_inheritance_column do
- # original_inheritance_column + "_id"
- # end
- # end
- def set_inheritance_column(value = nil, &block)
- define_attr_method :inheritance_column, value, &block
- end
- alias :inheritance_column= :set_inheritance_column
-
- # Sets the name of the sequence to use when generating ids to the given
- # value, or (if the value is nil or false) to the value returned by the
- # given block. This is required for Oracle and is useful for any
- # database which relies on sequences for primary key generation.
- #
- # If a sequence name is not explicitly set when using Oracle or Firebird,
- # it will default to the commonly used pattern of: #{table_name}_seq
- #
- # If a sequence name is not explicitly set when using PostgreSQL, it
- # will discover the sequence corresponding to your primary key for you.
- #
- # class Project < ActiveRecord::Base
- # set_sequence_name "projectseq" # default would have been "project_seq"
- # end
- def set_sequence_name(value = nil, &block)
- define_attr_method :sequence_name, value, &block
- end
- alias :sequence_name= :set_sequence_name
-
- # Indicates whether the table associated with this class exists
- def table_exists?
- connection.table_exists?(table_name)
- end
-
- # Returns an array of column objects for the table associated with this class.
- def columns
- if defined?(@primary_key)
- connection_pool.primary_keys[table_name] ||= @primary_key
- end
-
- connection_pool.columns[table_name]
- end
-
- # Returns a hash of column objects for the table associated with this class.
- def columns_hash
- connection_pool.columns_hash[table_name]
- end
-
- # Returns a hash where the keys are column names and the values are
- # default values when instantiating the AR object for this table.
- def column_defaults
- connection_pool.column_defaults[table_name]
- end
-
- # Returns an array of column names as strings.
- def column_names
- @column_names ||= columns.map { |column| column.name }
- end
-
- # Returns an array of column objects where the primary id, all columns ending in "_id" or "_count",
- # and columns used for single table inheritance have been removed.
- def content_columns
- @content_columns ||= columns.reject { |c| c.primary || c.name =~ /(_id|_count)$/ || c.name == inheritance_column }
- end
-
- # Returns a hash of all the methods added to query each of the columns in the table with the name of the method as the key
- # and true as the value. This makes it possible to do O(1) lookups in respond_to? to check if a given method for attribute
- # is available.
- def column_methods_hash #:nodoc:
- @dynamic_methods_hash ||= column_names.inject(Hash.new(false)) do |methods, attr|
- attr_name = attr.to_s
- methods[attr.to_sym] = attr_name
- methods["#{attr}=".to_sym] = attr_name
- methods["#{attr}?".to_sym] = attr_name
- methods["#{attr}_before_type_cast".to_sym] = attr_name
- methods
- end
- end
-
- # Resets all the cached information about columns, which will cause them
- # to be reloaded on the next request.
- #
- # The most common usage pattern for this method is probably in a migration,
- # when just after creating a table you want to populate it with some default
- # values, eg:
- #
- # class CreateJobLevels < ActiveRecord::Migration
- # def self.up
- # create_table :job_levels do |t|
- # t.integer :id
- # t.string :name
- #
- # t.timestamps
- # end
- #
- # JobLevel.reset_column_information
- # %w{assistant executive manager director}.each do |type|
- # JobLevel.create(:name => type)
- # end
- # end
- #
- # def self.down
- # drop_table :job_levels
- # end
- # end
- def reset_column_information
- connection.clear_cache!
- undefine_attribute_methods
- connection_pool.clear_table_cache!(table_name) if table_exists?
-
- @column_names = @content_columns = @dynamic_methods_hash = @inheritance_column = nil
- @arel_engine = @relation = nil
- end
-
- def clear_cache! # :nodoc:
- connection_pool.clear_cache!
- end
-
- def attribute_method?(attribute)
- super || (table_exists? && column_names.include?(attribute.to_s.sub(/=$/, '')))
- end
-
- # Returns an array of column names as strings if it's not
- # an abstract class and table exists.
- # Otherwise it returns an empty array.
- def attribute_names
- @attribute_names ||= if !abstract_class? && table_exists?
- column_names
- else
- []
- end
- end
-
- # Set the lookup ancestors for ActiveModel.
- def lookup_ancestors #:nodoc:
- klass = self
- classes = [klass]
- return classes if klass == ActiveRecord::Base
-
- while klass != klass.base_class
- classes << klass = klass.superclass
- end
- classes
- end
-
- # Set the i18n scope to overwrite ActiveModel.
- def i18n_scope #:nodoc:
- :activerecord
- end
-
- # True if this isn't a concrete subclass needing a STI type condition.
- def descends_from_active_record?
- if superclass.abstract_class?
- superclass.descends_from_active_record?
- else
- superclass == Base || !columns_hash.include?(inheritance_column)
- end
- end
-
- def finder_needs_type_condition? #:nodoc:
- # This is like this because benchmarking justifies the strange :false stuff
- :true == (@finder_needs_type_condition ||= descends_from_active_record? ? :false : :true)
- end
-
- # Returns a string like 'Post(id:integer, title:string, body:text)'
- def inspect
- if self == Base
- super
- elsif abstract_class?
- "#{super}(abstract)"
- elsif table_exists?
- attr_list = columns.map { |c| "#{c.name}: #{c.type}" } * ', '
- "#{super}(#{attr_list})"
- else
- "#{super}(Table doesn't exist)"
- end
- end
-
- def quote_value(value, column = nil) #:nodoc:
- connection.quote(value,column)
- end
-
- # Used to sanitize objects before they're used in an SQL SELECT statement. Delegates to <tt>connection.quote</tt>.
- def sanitize(object) #:nodoc:
- connection.quote(object)
- end
-
- # Overwrite the default class equality method to provide support for association proxies.
- def ===(object)
- object.is_a?(self)
- end
-
- def symbolized_base_class
- @symbolized_base_class ||= base_class.to_s.to_sym
- end
-
- def symbolized_sti_name
- @symbolized_sti_name ||= sti_name.present? ? sti_name.to_sym : symbolized_base_class
- end
-
- # Returns the base AR subclass that this class descends from. If A
- # extends AR::Base, A.base_class will return A. If B descends from A
- # through some arbitrarily deep hierarchy, B.base_class will return A.
- #
- # If B < A and C < B and if A is an abstract_class then both B.base_class
- # and C.base_class would return B as the answer since A is an abstract_class.
- def base_class
- class_of_active_record_descendant(self)
- end
-
- # Set this to true if this is an abstract class (see <tt>abstract_class?</tt>).
- attr_accessor :abstract_class
-
- # Returns whether this class is an abstract class or not.
- def abstract_class?
- defined?(@abstract_class) && @abstract_class == true
- end
-
- def respond_to?(method_id, include_private = false)
- if match = DynamicFinderMatch.match(method_id)
- return true if all_attributes_exists?(match.attribute_names)
- elsif match = DynamicScopeMatch.match(method_id)
- return true if all_attributes_exists?(match.attribute_names)
- end
-
- super
- end
-
- def sti_name
- store_full_sti_class ? name : name.demodulize
- end
-
- def arel_table
- @arel_table ||= Arel::Table.new(table_name, arel_engine)
- end
-
- def arel_engine
- @arel_engine ||= begin
- if self == ActiveRecord::Base
- ActiveRecord::Base
- else
- connection_handler.connection_pools[name] ? self : superclass.arel_engine
- end
- end
- end
-
- # Returns a scope for this class without taking into account the default_scope.
- #
- # class Post < ActiveRecord::Base
- # def self.default_scope
- # where :published => true
- # end
- # end
- #
- # Post.all # Fires "SELECT * FROM posts WHERE published = true"
- # Post.unscoped.all # Fires "SELECT * FROM posts"
- #
- # This method also accepts a block meaning that all queries inside the block will
- # not use the default_scope:
- #
- # Post.unscoped {
- # Post.limit(10) # Fires "SELECT * FROM posts LIMIT 10"
- # }
- #
- # It is recommended to use block form of unscoped because chaining unscoped with <tt>scope</tt>
- # does not work. Assuming that <tt>published</tt> is a <tt>scope</tt> following two statements are same.
- #
- # Post.unscoped.published
- # Post.published
- def unscoped #:nodoc:
- block_given? ? relation.scoping { yield } : relation
- end
-
- def before_remove_const #:nodoc:
- self.current_scope = nil
- end
-
- # Finder methods must instantiate through this method to work with the
- # single-table inheritance model that makes it possible to create
- # objects of different types from the same table.
- def instantiate(record)
- sti_class = find_sti_class(record[inheritance_column])
- record_id = sti_class.primary_key? && record[sti_class.primary_key]
-
- if ActiveRecord::IdentityMap.enabled? && record_id
- if (column = sti_class.columns_hash[sti_class.primary_key]) && column.number?
- record_id = record_id.to_i
- end
- if instance = IdentityMap.get(sti_class, record_id)
- instance.reinit_with('attributes' => record)
- else
- instance = sti_class.allocate.init_with('attributes' => record)
- IdentityMap.add(instance)
- end
- else
- instance = sti_class.allocate.init_with('attributes' => record)
- end
-
- instance
- end
-
- private
-
- def relation #:nodoc:
- @relation ||= Relation.new(self, arel_table)
-
- if finder_needs_type_condition?
- @relation.where(type_condition).create_with(inheritance_column.to_sym => sti_name)
- else
- @relation
- end
- end
-
- def find_sti_class(type_name)
- if type_name.blank? || !columns_hash.include?(inheritance_column)
- self
- else
- begin
- if store_full_sti_class
- ActiveSupport::Dependencies.constantize(type_name)
- else
- compute_type(type_name)
- end
- rescue NameError
- raise SubclassNotFound,
- "The single-table inheritance mechanism failed to locate the subclass: '#{type_name}'. " +
- "This error is raised because the column '#{inheritance_column}' is reserved for storing the class in case of inheritance. " +
- "Please rename this column if you didn't intend it to be used for storing the inheritance class " +
- "or overwrite #{name}.inheritance_column to use another column for that information."
- end
- end
- end
-
- def construct_finder_arel(options = {}, scope = nil)
- relation = options.is_a?(Hash) ? unscoped.apply_finder_options(options) : options
- relation = scope.merge(relation) if scope
- relation
- end
-
- def type_condition(table = arel_table)
- sti_column = table[inheritance_column.to_sym]
- sti_names = ([self] + descendants).map { |model| model.sti_name }
-
- sti_column.in(sti_names)
- end
-
- # Guesses the table name, but does not decorate it with prefix and suffix information.
- def undecorated_table_name(class_name = base_class.name)
- table_name = class_name.to_s.demodulize.underscore
- table_name = table_name.pluralize if pluralize_table_names
- table_name
- end
-
- # Computes and returns a table name according to default conventions.
- def compute_table_name
- base = base_class
- if self == base
- # Nested classes are prefixed with singular parent table name.
- if parent < ActiveRecord::Base && !parent.abstract_class?
- contained = parent.table_name
- contained = contained.singularize if parent.pluralize_table_names
- contained += '_'
- end
- "#{full_table_name_prefix}#{contained}#{undecorated_table_name(name)}#{table_name_suffix}"
- else
- # STI subclasses always use their superclass' table.
- base.table_name
- end
- end
-
- # Enables dynamic finders like <tt>User.find_by_user_name(user_name)</tt> and
- # <tt>User.scoped_by_user_name(user_name). Refer to Dynamic attribute-based finders
- # section at the top of this file for more detailed information.
- #
- # It's even possible to use all the additional parameters to +find+. For example, the
- # full interface for +find_all_by_amount+ is actually <tt>find_all_by_amount(amount, options)</tt>.
- #
- # Each dynamic finder using <tt>scoped_by_*</tt> is also defined in the class after it
- # is first invoked, so that future attempts to use it do not run through method_missing.
- def method_missing(method_id, *arguments, &block)
- if match = (DynamicFinderMatch.match(method_id) || DynamicScopeMatch.match(method_id))
- attribute_names = match.attribute_names
- super unless all_attributes_exists?(attribute_names)
- if arguments.size < attribute_names.size
- method_trace = "#{__FILE__}:#{__LINE__}:in `#{method_id}'"
- backtrace = [method_trace] + caller
- raise ArgumentError, "wrong number of arguments (#{arguments.size} for #{attribute_names.size})", backtrace
- end
- if match.respond_to?(:scope?) && match.scope?
- self.class_eval <<-METHOD, __FILE__, __LINE__ + 1
- def self.#{method_id}(*args) # def self.scoped_by_user_name_and_password(*args)
- attributes = Hash[[:#{attribute_names.join(',:')}].zip(args)] # attributes = Hash[[:user_name, :password].zip(args)]
- #
- scoped(:conditions => attributes) # scoped(:conditions => attributes)
- end # end
- METHOD
- send(method_id, *arguments)
- elsif match.finder?
- options = arguments.extract_options!
- relation = options.any? ? scoped(options) : scoped
- relation.send :find_by_attributes, match, attribute_names, *arguments, &block
- elsif match.instantiator?
- scoped.send :find_or_instantiator_by_attributes, match, attribute_names, *arguments, &block
- end
- else
- super
- end
- end
-
- # Similar in purpose to +expand_hash_conditions_for_aggregates+.
- def expand_attribute_names_for_aggregates(attribute_names)
- attribute_names.map { |attribute_name|
- unless (aggregation = reflect_on_aggregation(attribute_name.to_sym)).nil?
- aggregate_mapping(aggregation).map do |field_attr, _|
- field_attr.to_sym
- end
- else
- attribute_name.to_sym
- end
- }.flatten
- end
-
- def all_attributes_exists?(attribute_names)
- (expand_attribute_names_for_aggregates(attribute_names) -
- column_methods_hash.keys).empty?
- end
-
- protected
- # with_scope lets you apply options to inner block incrementally. It takes a hash and the keys must be
- # <tt>:find</tt> or <tt>:create</tt>. <tt>:find</tt> parameter is <tt>Relation</tt> while
- # <tt>:create</tt> parameters are an attributes hash.
- #
- # class Article < ActiveRecord::Base
- # def self.create_with_scope
- # with_scope(:find => where(:blog_id => 1), :create => { :blog_id => 1 }) do
- # find(1) # => SELECT * from articles WHERE blog_id = 1 AND id = 1
- # a = create(1)
- # a.blog_id # => 1
- # end
- # end
- # end
- #
- # In nested scopings, all previous parameters are overwritten by the innermost rule, with the exception of
- # <tt>where</tt>, <tt>includes</tt>, and <tt>joins</tt> operations in <tt>Relation</tt>, which are merged.
- #
- # <tt>joins</tt> operations are uniqued so multiple scopes can join in the same table without table aliasing
- # problems. If you need to join multiple tables, but still want one of the tables to be uniqued, use the
- # array of strings format for your joins.
- #
- # class Article < ActiveRecord::Base
- # def self.find_with_scope
- # with_scope(:find => where(:blog_id => 1).limit(1), :create => { :blog_id => 1 }) do
- # with_scope(:find => limit(10)) do
- # all # => SELECT * from articles WHERE blog_id = 1 LIMIT 10
- # end
- # with_scope(:find => where(:author_id => 3)) do
- # all # => SELECT * from articles WHERE blog_id = 1 AND author_id = 3 LIMIT 1
- # end
- # end
- # end
- # end
- #
- # You can ignore any previous scopings by using the <tt>with_exclusive_scope</tt> method.
- #
- # class Article < ActiveRecord::Base
- # def self.find_with_exclusive_scope
- # with_scope(:find => where(:blog_id => 1).limit(1)) do
- # with_exclusive_scope(:find => limit(10)) do
- # all # => SELECT * from articles LIMIT 10
- # end
- # end
- # end
- # end
- #
- # *Note*: the +:find+ scope also has effect on update and deletion methods, like +update_all+ and +delete_all+.
- def with_scope(scope = {}, action = :merge, &block)
- # If another Active Record class has been passed in, get its current scope
- scope = scope.current_scope if !scope.is_a?(Relation) && scope.respond_to?(:current_scope)
-
- previous_scope = self.current_scope
-
- if scope.is_a?(Hash)
- # Dup first and second level of hash (method and params).
- scope = scope.dup
- scope.each do |method, params|
- scope[method] = params.dup unless params == true
- end
-
- scope.assert_valid_keys([ :find, :create ])
- relation = construct_finder_arel(scope[:find] || {})
- relation.default_scoped = true unless action == :overwrite
-
- if previous_scope && previous_scope.create_with_value && scope[:create]
- scope_for_create = if action == :merge
- previous_scope.create_with_value.merge(scope[:create])
- else
- scope[:create]
- end
-
- relation = relation.create_with(scope_for_create)
- else
- scope_for_create = scope[:create]
- scope_for_create ||= previous_scope.create_with_value if previous_scope
- relation = relation.create_with(scope_for_create) if scope_for_create
- end
-
- scope = relation
- end
-
- scope = previous_scope.merge(scope) if previous_scope && action == :merge
-
- self.current_scope = scope
- begin
- yield
- ensure
- self.current_scope = previous_scope
- end
- end
-
- # Works like with_scope, but discards any nested properties.
- def with_exclusive_scope(method_scoping = {}, &block)
- if method_scoping.values.any? { |e| e.is_a?(ActiveRecord::Relation) }
- raise ArgumentError, <<-MSG
-New finder API can not be used with_exclusive_scope. You can either call unscoped to get an anonymous scope not bound to the default_scope:
-
- User.unscoped.where(:active => true)
-
-Or call unscoped with a block:
-
- User.unscoped do
- User.where(:active => true).all
- end
-
-MSG
- end
- with_scope(method_scoping, :overwrite, &block)
- end
-
- def current_scope #:nodoc:
- Thread.current["#{self}_current_scope"]
- end
-
- def current_scope=(scope) #:nodoc:
- Thread.current["#{self}_current_scope"] = scope
- end
-
- # Use this macro in your model to set a default scope for all operations on
- # the model.
- #
- # class Article < ActiveRecord::Base
- # default_scope where(:published => true)
- # end
- #
- # Article.all # => SELECT * FROM articles WHERE published = true
- #
- # The <tt>default_scope</tt> is also applied while creating/building a record. It is not
- # applied while updating a record.
- #
- # Article.new.published # => true
- # Article.create.published # => true
- #
- # You can also use <tt>default_scope</tt> with a block, in order to have it lazily evaluated:
- #
- # class Article < ActiveRecord::Base
- # default_scope { where(:published_at => Time.now - 1.week) }
- # end
- #
- # (You can also pass any object which responds to <tt>call</tt> to the <tt>default_scope</tt>
- # macro, and it will be called when building the default scope.)
- #
- # If you use multiple <tt>default_scope</tt> declarations in your model then they will
- # be merged together:
- #
- # class Article < ActiveRecord::Base
- # default_scope where(:published => true)
- # default_scope where(:rating => 'G')
- # end
- #
- # Article.all # => SELECT * FROM articles WHERE published = true AND rating = 'G'
- #
- # This is also the case with inheritance and module includes where the parent or module
- # defines a <tt>default_scope</tt> and the child or including class defines a second one.
- #
- # If you need to do more complex things with a default scope, you can alternatively
- # define it as a class method:
- #
- # class Article < ActiveRecord::Base
- # def self.default_scope
- # # Should return a scope, you can call 'super' here etc.
- # end
- # end
- def default_scope(scope = {})
- scope = Proc.new if block_given?
- self.default_scopes = default_scopes + [scope]
- end
-
- def build_default_scope #:nodoc:
- if method(:default_scope).owner != Base.singleton_class
- evaluate_default_scope { default_scope }
- elsif default_scopes.any?
- evaluate_default_scope do
- default_scopes.inject(relation) do |default_scope, scope|
- if scope.is_a?(Hash)
- default_scope.apply_finder_options(scope)
- elsif !scope.is_a?(Relation) && scope.respond_to?(:call)
- default_scope.merge(scope.call)
- else
- default_scope.merge(scope)
- end
- end
- end
- end
- end
-
- def ignore_default_scope? #:nodoc:
- Thread.current["#{self}_ignore_default_scope"]
- end
-
- def ignore_default_scope=(ignore) #:nodoc:
- Thread.current["#{self}_ignore_default_scope"] = ignore
- end
-
- # The ignore_default_scope flag is used to prevent an infinite recursion situation where
- # a default scope references a scope which has a default scope which references a scope...
- def evaluate_default_scope
- return if ignore_default_scope?
-
- begin
- self.ignore_default_scope = true
- yield
- ensure
- self.ignore_default_scope = false
- end
- end
-
- # Returns the class type of the record using the current module as a prefix. So descendants of
- # MyApp::Business::Account would appear as MyApp::Business::AccountSubclass.
- def compute_type(type_name)
- if type_name.match(/^::/)
- # If the type is prefixed with a scope operator then we assume that
- # the type_name is an absolute reference.
- ActiveSupport::Dependencies.constantize(type_name)
- else
- # Build a list of candidates to search for
- candidates = []
- name.scan(/::|$/) { candidates.unshift "#{$`}::#{type_name}" }
- candidates << type_name
-
- candidates.each do |candidate|
- begin
- constant = ActiveSupport::Dependencies.constantize(candidate)
- return constant if candidate == constant.to_s
- rescue NameError => e
- # We don't want to swallow NoMethodError < NameError errors
- raise e unless e.instance_of?(NameError)
- end
- end
-
- raise NameError, "uninitialized constant #{candidates.first}"
- end
- end
-
- # Returns the class descending directly from ActiveRecord::Base or an
- # abstract class, if any, in the inheritance hierarchy.
- def class_of_active_record_descendant(klass)
- if klass == Base || klass.superclass == Base || klass.superclass.abstract_class?
- klass
- elsif klass.superclass.nil?
- raise ActiveRecordError, "#{name} doesn't belong in a hierarchy descending from ActiveRecord"
- else
- class_of_active_record_descendant(klass.superclass)
- end
- end
-
- # Accepts an array, hash, or string of SQL conditions and sanitizes
- # them into a valid SQL fragment for a WHERE clause.
- # ["name='%s' and group_id='%s'", "foo'bar", 4] returns "name='foo''bar' and group_id='4'"
- # { :name => "foo'bar", :group_id => 4 } returns "name='foo''bar' and group_id='4'"
- # "name='foo''bar' and group_id='4'" returns "name='foo''bar' and group_id='4'"
- def sanitize_sql_for_conditions(condition, table_name = self.table_name)
- return nil if condition.blank?
-
- case condition
- when Array; sanitize_sql_array(condition)
- when Hash; sanitize_sql_hash_for_conditions(condition, table_name)
- else condition
- end
- end
- alias_method :sanitize_sql, :sanitize_sql_for_conditions
-
- # Accepts an array, hash, or string of SQL conditions and sanitizes
- # them into a valid SQL fragment for a SET clause.
- # { :name => nil, :group_id => 4 } returns "name = NULL , group_id='4'"
- def sanitize_sql_for_assignment(assignments)
- case assignments
- when Array; sanitize_sql_array(assignments)
- when Hash; sanitize_sql_hash_for_assignment(assignments)
- else assignments
- end
- end
-
- def aggregate_mapping(reflection)
- mapping = reflection.options[:mapping] || [reflection.name, reflection.name]
- mapping.first.is_a?(Array) ? mapping : [mapping]
- end
-
- # Accepts a hash of SQL conditions and replaces those attributes
- # that correspond to a +composed_of+ relationship with their expanded
- # aggregate attribute values.
- # Given:
- # class Person < ActiveRecord::Base
- # composed_of :address, :class_name => "Address",
- # :mapping => [%w(address_street street), %w(address_city city)]
- # end
- # Then:
- # { :address => Address.new("813 abc st.", "chicago") }
- # # => { :address_street => "813 abc st.", :address_city => "chicago" }
- def expand_hash_conditions_for_aggregates(attrs)
- expanded_attrs = {}
- attrs.each do |attr, value|
- unless (aggregation = reflect_on_aggregation(attr.to_sym)).nil?
- mapping = aggregate_mapping(aggregation)
- mapping.each do |field_attr, aggregate_attr|
- if mapping.size == 1 && !value.respond_to?(aggregate_attr)
- expanded_attrs[field_attr] = value
- else
- expanded_attrs[field_attr] = value.send(aggregate_attr)
- end
- end
- else
- expanded_attrs[attr] = value
- end
- end
- expanded_attrs
- end
-
- # Sanitizes a hash of attribute/value pairs into SQL conditions for a WHERE clause.
- # { :name => "foo'bar", :group_id => 4 }
- # # => "name='foo''bar' and group_id= 4"
- # { :status => nil, :group_id => [1,2,3] }
- # # => "status IS NULL and group_id IN (1,2,3)"
- # { :age => 13..18 }
- # # => "age BETWEEN 13 AND 18"
- # { 'other_records.id' => 7 }
- # # => "`other_records`.`id` = 7"
- # { :other_records => { :id => 7 } }
- # # => "`other_records`.`id` = 7"
- # And for value objects on a composed_of relationship:
- # { :address => Address.new("123 abc st.", "chicago") }
- # # => "address_street='123 abc st.' and address_city='chicago'"
- def sanitize_sql_hash_for_conditions(attrs, default_table_name = self.table_name)
- attrs = expand_hash_conditions_for_aggregates(attrs)
-
- table = Arel::Table.new(table_name).alias(default_table_name)
- PredicateBuilder.build_from_hash(arel_engine, attrs, table).map { |b|
- connection.visitor.accept b
- }.join(' AND ')
- end
- alias_method :sanitize_sql_hash, :sanitize_sql_hash_for_conditions
-
- # Sanitizes a hash of attribute/value pairs into SQL conditions for a SET clause.
- # { :status => nil, :group_id => 1 }
- # # => "status = NULL , group_id = 1"
- def sanitize_sql_hash_for_assignment(attrs)
- attrs.map do |attr, value|
- "#{connection.quote_column_name(attr)} = #{quote_bound_value(value)}"
- end.join(', ')
- end
-
- # Accepts an array of conditions. The array has each value
- # sanitized and interpolated into the SQL statement.
- # ["name='%s' and group_id='%s'", "foo'bar", 4] returns "name='foo''bar' and group_id='4'"
- def sanitize_sql_array(ary)
- statement, *values = ary
- if values.first.is_a?(Hash) && statement =~ /:\w+/
- replace_named_bind_variables(statement, values.first)
- elsif statement.include?('?')
- replace_bind_variables(statement, values)
- elsif statement.blank?
- statement
- else
- statement % values.collect { |value| connection.quote_string(value.to_s) }
- end
- end
-
- alias_method :sanitize_conditions, :sanitize_sql
-
- def replace_bind_variables(statement, values) #:nodoc:
- raise_if_bind_arity_mismatch(statement, statement.count('?'), values.size)
- bound = values.dup
- c = connection
- statement.gsub('?') { quote_bound_value(bound.shift, c) }
- end
-
- def replace_named_bind_variables(statement, bind_vars) #:nodoc:
- statement.gsub(/(:?):([a-zA-Z]\w*)/) do
- if $1 == ':' # skip postgresql casts
- $& # return the whole match
- elsif bind_vars.include?(match = $2.to_sym)
- quote_bound_value(bind_vars[match])
- else
- raise PreparedStatementInvalid, "missing value for :#{match} in #{statement}"
- end
- end
- end
-
- def expand_range_bind_variables(bind_vars) #:nodoc:
- expanded = []
-
- bind_vars.each do |var|
- next if var.is_a?(Hash)
-
- if var.is_a?(Range)
- expanded << var.first
- expanded << var.last
- else
- expanded << var
- end
- end
-
- expanded
- end
-
- def quote_bound_value(value, c = connection) #:nodoc:
- if value.respond_to?(:map) && !value.acts_like?(:string)
- if value.respond_to?(:empty?) && value.empty?
- c.quote(nil)
- else
- value.map { |v| c.quote(v) }.join(',')
- end
- else
- c.quote(value)
- end
- end
-
- def raise_if_bind_arity_mismatch(statement, expected, provided) #:nodoc:
- unless expected == provided
- raise PreparedStatementInvalid, "wrong number of bind variables (#{provided} for #{expected}) in: #{statement}"
- end
- end
-
- def encode_quoted_value(value) #:nodoc:
- quoted_value = connection.quote(value)
- quoted_value = "'#{quoted_value[1..-2].gsub(/\'/, "\\\\'")}'" if quoted_value.include?("\\\'") # (for ruby mode) "
- quoted_value
- end
- end
-
- public
- # New objects can be instantiated as either empty (pass no construction parameter) or pre-set with
- # attributes but not yet saved (pass a hash with key names matching the associated table column names).
- # In both instances, valid attribute keys are determined by the column names of the associated table --
- # hence you can't have attributes that aren't part of the table columns.
- #
- # +initialize+ respects mass-assignment security and accepts either +:as+ or +:without_protection+ options
- # in the +options+ parameter.
- #
- # ==== Examples
- # # Instantiates a single new object
- # User.new(:first_name => 'Jamie')
- #
- # # Instantiates a single new object using the :admin mass-assignment security role
- # User.new({ :first_name => 'Jamie', :is_admin => true }, :as => :admin)
- #
- # # Instantiates a single new object bypassing mass-assignment security
- # User.new({ :first_name => 'Jamie', :is_admin => true }, :without_protection => true)
- def initialize(attributes = nil, options = {})
- @attributes = attributes_from_column_definition
- @association_cache = {}
- @aggregation_cache = {}
- @attributes_cache = {}
- @new_record = true
- @readonly = false
- @destroyed = false
- @marked_for_destruction = false
- @previously_changed = {}
- @changed_attributes = {}
- @relation = nil
-
- ensure_proper_type
- set_serialized_attributes
-
- populate_with_current_scope_attributes
-
- assign_attributes(attributes, options) if attributes
-
- yield self if block_given?
- run_callbacks :initialize
- end
-
- # Populate +coder+ with attributes about this record that should be
- # serialized. The structure of +coder+ defined in this method is
- # guaranteed to match the structure of +coder+ passed to the +init_with+
- # method.
- #
- # Example:
- #
- # class Post < ActiveRecord::Base
- # end
- # coder = {}
- # Post.new.encode_with(coder)
- # coder # => { 'id' => nil, ... }
- def encode_with(coder)
- coder['attributes'] = attributes
- end
-
- # Initialize an empty model object from +coder+. +coder+ must contain
- # the attributes necessary for initializing an empty model object. For
- # example:
- #
- # class Post < ActiveRecord::Base
- # end
- #
- # post = Post.allocate
- # post.init_with('attributes' => { 'title' => 'hello world' })
- # post.title # => 'hello world'
- def init_with(coder)
- @attributes = coder['attributes']
- @relation = nil
-
- set_serialized_attributes
-
- @attributes_cache, @previously_changed, @changed_attributes = {}, {}, {}
- @association_cache = {}
- @aggregation_cache = {}
- @readonly = @destroyed = @marked_for_destruction = false
- @new_record = false
- run_callbacks :find
- run_callbacks :initialize
-
- self
- end
-
- # Returns a String, which Action Pack uses for constructing an URL to this
- # object. The default implementation returns this record's id as a String,
- # or nil if this record's unsaved.
- #
- # For example, suppose that you have a User model, and that you have a
- # <tt>resources :users</tt> route. Normally, +user_path+ will
- # construct a path with the user object's 'id' in it:
- #
- # user = User.find_by_name('Phusion')
- # user_path(user) # => "/users/1"
- #
- # You can override +to_param+ in your model to make +user_path+ construct
- # a path using the user's name instead of the user's id:
- #
- # class User < ActiveRecord::Base
- # def to_param # overridden
- # name
- # end
- # end
- #
- # user = User.find_by_name('Phusion')
- # user_path(user) # => "/users/Phusion"
- def to_param
- # We can't use alias_method here, because method 'id' optimizes itself on the fly.
- id && id.to_s # Be sure to stringify the id for routes
- end
-
- # Returns a cache key that can be used to identify this record.
- #
- # ==== Examples
- #
- # 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
- case
- when new_record?
- "#{self.class.model_name.cache_key}/new"
- when timestamp = self[:updated_at]
- timestamp = timestamp.utc.to_s(:number)
- "#{self.class.model_name.cache_key}/#{id}-#{timestamp}"
- else
- "#{self.class.model_name.cache_key}/#{id}"
- end
- end
-
- def quoted_id #:nodoc:
- quote_value(id, column_for_attribute(self.class.primary_key))
- end
-
- # Returns true if the given attribute is in the attributes hash
- def has_attribute?(attr_name)
- @attributes.has_key?(attr_name.to_s)
- end
-
- # Returns an array of names for the attributes available on this object.
- def attribute_names
- @attributes.keys
- end
-
- # Allows you to set all the attributes at once by passing in a hash with keys
- # matching the attribute names (which again matches the column names).
- #
- # If any attributes are protected by either +attr_protected+ or
- # +attr_accessible+ then only settable attributes will be assigned.
- #
- # class User < ActiveRecord::Base
- # attr_protected :is_admin
- # end
- #
- # user = User.new
- # user.attributes = { :username => 'Phusion', :is_admin => true }
- # user.username # => "Phusion"
- # user.is_admin? # => false
- def attributes=(new_attributes)
- return unless new_attributes.is_a?(Hash)
-
- assign_attributes(new_attributes)
- end
-
- # Allows you to set all the attributes for a particular mass-assignment
- # security role by passing in a hash of attributes with keys matching
- # the attribute names (which again matches the column names) and the role
- # name using the :as option.
- #
- # To bypass mass-assignment security you can use the :without_protection => true
- # option.
- #
- # class User < ActiveRecord::Base
- # attr_accessible :name
- # attr_accessible :name, :is_admin, :as => :admin
- # end
- #
- # user = User.new
- # user.assign_attributes({ :name => 'Josh', :is_admin => true })
- # user.name # => "Josh"
- # user.is_admin? # => false
- #
- # user = User.new
- # user.assign_attributes({ :name => 'Josh', :is_admin => true }, :as => :admin)
- # user.name # => "Josh"
- # user.is_admin? # => true
- #
- # user = User.new
- # user.assign_attributes({ :name => 'Josh', :is_admin => true }, :without_protection => true)
- # user.name # => "Josh"
- # user.is_admin? # => true
- def assign_attributes(new_attributes, options = {})
- return unless new_attributes
-
- attributes = new_attributes.stringify_keys
- multi_parameter_attributes = []
- @mass_assignment_options = options
-
- unless options[:without_protection]
- attributes = sanitize_for_mass_assignment(attributes, mass_assignment_role)
- end
-
- attributes.each do |k, v|
- if k.include?("(")
- multi_parameter_attributes << [ k, v ]
- elsif respond_to?("#{k}=")
- send("#{k}=", v)
- else
- raise(UnknownAttributeError, "unknown attribute: #{k}")
- end
- end
-
- @mass_assignment_options = nil
- assign_multiparameter_attributes(multi_parameter_attributes)
- end
-
- # Returns a hash of all the attributes with their names as keys and the values of the attributes as values.
- def attributes
- Hash[@attributes.map { |name, _| [name, read_attribute(name)] }]
- end
-
- # 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.
- #
- # person = Person.create!(:name => "David Heinemeier Hansson " * 3)
- #
- # person.attribute_for_inspect(:name)
- # # => '"David Heinemeier Hansson David Heinemeier Hansson D..."'
- #
- # person.attribute_for_inspect(:created_at)
- # # => '"2009-01-12 04:48:57"'
- def attribute_for_inspect(attr_name)
- value = read_attribute(attr_name)
-
- if value.is_a?(String) && value.length > 50
- "#{value[0..50]}...".inspect
- elsif value.is_a?(Date) || value.is_a?(Time)
- %("#{value.to_s(:db)}")
- else
- value.inspect
- end
- end
-
- # Returns true if the specified +attribute+ has been set by the user or by a database load and is neither
- # nil nor empty? (the latter only applies to objects that respond to empty?, most notably Strings).
- def attribute_present?(attribute)
- !_read_attribute(attribute).blank?
- end
-
- # Returns the column object for the named attribute.
- def column_for_attribute(name)
- self.class.columns_hash[name.to_s]
- end
-
- # Returns true if +comparison_object+ is the same exact object, or +comparison_object+
- # is of the same type and +self+ has an ID and it is equal to +comparison_object.id+.
- #
- # Note that new records are different from any other record by definition, unless the
- # other record is the receiver itself. Besides, if you fetch existing records with
- # +select+ and leave the ID out, you're on your own, this predicate will return false.
- #
- # Note also that destroying a record preserves its ID in the model instance, so deleted
- # models are still comparable.
- def ==(comparison_object)
- super ||
- comparison_object.instance_of?(self.class) &&
- id.present? &&
- comparison_object.id == id
- end
- alias :eql? :==
-
- # Delegates to id in order to allow two records of the same type and id to work with something like:
- # [ Person.find(1), Person.find(2), Person.find(3) ] & [ Person.find(1), Person.find(4) ] # => [ Person.find(1) ]
- def hash
- id.hash
- end
-
- # Freeze the attributes hash such that associations are still accessible, even on destroyed records.
- def freeze
- @attributes.freeze; self
- end
-
- # Returns +true+ if the attributes hash has been frozen.
- def frozen?
- @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
- nil
- end
- end
-
- # Backport dup from 1.9 so that initialize_dup() gets called
- unless Object.respond_to?(:initialize_dup)
- def dup # :nodoc:
- copy = super
- copy.initialize_dup(self)
- copy
- end
- end
-
- # Duped objects have no id assigned and are treated as new records. Note
- # that this is a "shallow" copy as it copies the object's attributes
- # only, not its associations. The extent of a "deep" copy is application
- # specific and is therefore left to the application to implement according
- # to its need.
- # The dup method does not preserve the timestamps (created|updated)_(at|on).
- def initialize_dup(other)
- cloned_attributes = other.clone_attributes(:read_attribute_before_type_cast)
- cloned_attributes.delete(self.class.primary_key)
-
- @attributes = cloned_attributes
-
- _run_after_initialize_callbacks if respond_to?(:_run_after_initialize_callbacks)
-
- @changed_attributes = {}
- attributes_from_column_definition.each do |attr, orig_value|
- @changed_attributes[attr] = orig_value if field_changed?(attr, orig_value, @attributes[attr])
- end
-
- @aggregation_cache = {}
- @association_cache = {}
- @attributes_cache = {}
- @new_record = true
-
- ensure_proper_type
- populate_with_current_scope_attributes
- super
- 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?
- @readonly
- end
-
- # Marks this record as read only.
- def readonly!
- @readonly = true
- end
-
- # Returns the contents of the record as a nicely formatted string.
- def inspect
- inspection = if @attributes
- self.class.column_names.collect { |name|
- if has_attribute?(name)
- "#{name}: #{attribute_for_inspect(name)}"
- end
- }.compact.join(", ")
- else
- "not initialized"
- end
- "#<#{self.class} #{inspection}>"
- end
-
- protected
- def clone_attributes(reader_method = :read_attribute, attributes = {})
- attribute_names.each do |name|
- attributes[name] = clone_attribute_value(reader_method, name)
- end
- attributes
- end
-
- def clone_attribute_value(reader_method, attribute_name)
- value = send(reader_method, attribute_name)
- value.duplicable? ? value.clone : value
- rescue TypeError, NoMethodError
- value
- end
-
- def mass_assignment_options
- @mass_assignment_options ||= {}
- end
-
- def mass_assignment_role
- mass_assignment_options[:as] || :default
- end
-
- private
-
- # Under Ruby 1.9, Array#flatten will call #to_ary (recursively) on each of the elements
- # of the array, and then rescues from the possible NoMethodError. If those elements are
- # ActiveRecord::Base's, then this triggers the various method_missing's that we have,
- # which significantly impacts upon performance.
- #
- # So we can avoid the method_missing hit by explicitly defining #to_ary as nil here.
- #
- # See also http://tenderlovemaking.com/2011/06/28/til-its-ok-to-return-nil-from-to_ary/
- def to_ary # :nodoc:
- nil
- end
-
- def set_serialized_attributes
- sattrs = self.class.serialized_attributes
-
- sattrs.each do |key, coder|
- @attributes[key] = coder.load @attributes[key] if @attributes.key?(key)
- end
- end
-
- # Sets the attribute used for single table inheritance to this class name if this is not the
- # ActiveRecord::Base descendant.
- # Considering the hierarchy Reply < Message < ActiveRecord::Base, this makes it possible to
- # do Reply.new without having to set <tt>Reply[Reply.inheritance_column] = "Reply"</tt> yourself.
- # No such attribute would be set for objects of the Message class in that example.
- def ensure_proper_type
- klass = self.class
- if klass.finder_needs_type_condition?
- write_attribute(klass.inheritance_column, klass.sti_name)
- end
- end
-
- # The primary key and inheritance column can never be set by mass-assignment for security reasons.
- def self.attributes_protected_by_default
- default = [ inheritance_column ]
- default << primary_key if primary_key?
- default << 'id' unless primary_key? && primary_key == 'id'
- default
- end
-
- # Returns a copy of the attributes hash where all the values have been safely quoted for use in
- # an Arel insert/update method.
- def arel_attributes_values(include_primary_key = true, include_readonly_attributes = true, attribute_names = @attributes.keys)
- attrs = {}
- klass = self.class
- arel_table = klass.arel_table
-
- attribute_names.each do |name|
- if (column = column_for_attribute(name)) && (include_primary_key || !column.primary)
-
- if include_readonly_attributes || (!include_readonly_attributes && !self.class.readonly_attributes.include?(name))
-
- value = if coder = klass.serialized_attributes[name]
- coder.dump @attributes[name]
- else
- # FIXME: we need @attributes to be used consistently.
- # If the values stored in @attributes were already type
- # casted, this code could be simplified
- read_attribute(name)
- end
-
- attrs[arel_table[name]] = value
- end
- end
- end
- attrs
- end
-
- # Quote strings appropriately for SQL statements.
- def quote_value(value, column = nil)
- self.class.connection.quote(value, column)
- end
-
- # Instantiates objects for all attribute classes that needs more than one constructor parameter. This is done
- # by calling new on the column type or aggregation type (through composed_of) object with these parameters.
- # So having the pairs written_on(1) = "2004", written_on(2) = "6", written_on(3) = "24", will instantiate
- # written_on (a date type) with Date.new("2004", "6", "24"). You can also specify a typecast character in the
- # parentheses to have the parameters typecasted before they're used in the constructor. Use i for Fixnum,
- # f for Float, s for String, and a for Array. If all the values for a given attribute are empty, the
- # attribute will be set to nil.
- def assign_multiparameter_attributes(pairs)
- execute_callstack_for_multiparameter_attributes(
- extract_callstack_for_multiparameter_attributes(pairs)
- )
- end
-
- def instantiate_time_object(name, values)
- if self.class.send(:create_time_zone_conversion_attribute?, name, column_for_attribute(name))
- Time.zone.local(*values)
- else
- Time.time_with_datetime_fallback(@@default_timezone, *values)
- end
- end
-
- def execute_callstack_for_multiparameter_attributes(callstack)
- errors = []
- callstack.each do |name, values_with_empty_parameters|
- begin
- send(name + "=", read_value_from_parameter(name, values_with_empty_parameters))
- rescue => ex
- errors << AttributeAssignmentError.new("error on assignment #{values_with_empty_parameters.values.inspect} to #{name}", ex, name)
- end
- end
- unless errors.empty?
- raise MultiparameterAssignmentErrors.new(errors), "#{errors.size} error(s) on assignment of multiparameter attributes"
- end
- end
-
- def read_value_from_parameter(name, values_hash_from_param)
- klass = (self.class.reflect_on_aggregation(name.to_sym) || column_for_attribute(name)).klass
- if values_hash_from_param.values.all?{|v|v.nil?}
- nil
- elsif klass == Time
- read_time_parameter_value(name, values_hash_from_param)
- elsif klass == Date
- read_date_parameter_value(name, values_hash_from_param)
- else
- read_other_parameter_value(klass, name, values_hash_from_param)
- end
- end
-
- def read_time_parameter_value(name, values_hash_from_param)
- # If Date bits were not provided, error
- raise "Missing Parameter" if [1,2,3].any?{|position| !values_hash_from_param.has_key?(position)}
- max_position = extract_max_param_for_multiparameter_attributes(values_hash_from_param, 6)
- # If Date bits were provided but blank, then return nil
- return nil if (1..3).any? {|position| values_hash_from_param[position].blank?}
-
- set_values = (1..max_position).collect{|position| values_hash_from_param[position] }
- # If Time bits are not there, then default to 0
- (3..5).each {|i| set_values[i] = set_values[i].blank? ? 0 : set_values[i]}
- instantiate_time_object(name, set_values)
- end
-
- def read_date_parameter_value(name, values_hash_from_param)
- return nil if (1..3).any? {|position| values_hash_from_param[position].blank?}
- set_values = [values_hash_from_param[1], values_hash_from_param[2], values_hash_from_param[3]]
- begin
- Date.new(*set_values)
- rescue ArgumentError # if Date.new raises an exception on an invalid date
- instantiate_time_object(name, set_values).to_date # we instantiate Time object and convert it back to a date thus using Time's logic in handling invalid dates
- end
- end
-
- def read_other_parameter_value(klass, name, values_hash_from_param)
- max_position = extract_max_param_for_multiparameter_attributes(values_hash_from_param)
- values = (1..max_position).collect do |position|
- raise "Missing Parameter" if !values_hash_from_param.has_key?(position)
- values_hash_from_param[position]
- end
- klass.new(*values)
- end
-
- def extract_max_param_for_multiparameter_attributes(values_hash_from_param, upper_cap = 100)
- [values_hash_from_param.keys.max,upper_cap].min
- end
-
- def extract_callstack_for_multiparameter_attributes(pairs)
- attributes = { }
-
- pairs.each do |pair|
- multiparameter_name, value = pair
- attribute_name = multiparameter_name.split("(").first
- attributes[attribute_name] = {} unless attributes.include?(attribute_name)
-
- parameter_value = value.empty? ? nil : type_cast_attribute_value(multiparameter_name, value)
- attributes[attribute_name][find_parameter_position(multiparameter_name)] ||= parameter_value
- end
-
- attributes
- end
-
- def type_cast_attribute_value(multiparameter_name, value)
- multiparameter_name =~ /\([0-9]*([if])\)/ ? value.send("to_" + $1) : value
- end
-
- def find_parameter_position(multiparameter_name)
- multiparameter_name.scan(/\(([0-9]*).*\)/).first.first.to_i
- end
-
- # Returns a comma-separated pair list, like "key1 = val1, key2 = val2".
- def comma_pair_list(hash)
- hash.map { |k,v| "#{k} = #{v}" }.join(", ")
- end
-
- def quote_columns(quoter, hash)
- Hash[hash.map { |name, value| [quoter.quote_column_name(name), value] }]
- end
-
- def quoted_comma_pair_list(quoter, hash)
- comma_pair_list(quote_columns(quoter, hash))
- end
-
- def convert_number_column_value(value)
- if value == false
- 0
- elsif value == true
- 1
- elsif value.is_a?(String) && value.blank?
- nil
- else
- value
- end
- end
-
- def populate_with_current_scope_attributes
- return unless self.class.scope_attributes?
-
- self.class.scope_attributes.each do |att,value|
- send("#{att}=", value) if respond_to?("#{att}=")
- end
- end
- end
-
- Base.class_eval do
- include ActiveRecord::Persistence
- extend ActiveModel::Naming
- extend QueryCache::ClassMethods
- extend ActiveSupport::Benchmarkable
- extend ActiveSupport::DescendantsTracker
-
- include ActiveModel::Conversion
- include Validations
- extend CounterCache
- include Locking::Optimistic, Locking::Pessimistic
- include AttributeMethods
- include AttributeMethods::Read, AttributeMethods::Write, AttributeMethods::BeforeTypeCast, AttributeMethods::Query
- include AttributeMethods::PrimaryKey
- include AttributeMethods::TimeZoneConversion
- include AttributeMethods::Dirty
- include ActiveModel::MassAssignmentSecurity
- include Callbacks, ActiveModel::Observing, Timestamp
- include Associations, NamedScope
- include IdentityMap
- include ActiveModel::SecurePassword
-
- # AutosaveAssociation needs to be included before Transactions, because we want
- # #save_with_autosave_associations to be wrapped inside a transaction.
- include AutosaveAssociation, NestedAttributes
- include Aggregations, Transactions, Reflection, Serialization
-
- NilClass.add_whiner(self) if NilClass.respond_to?(:add_whiner)
-
- # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example,
- # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)).
- # (Alias for the protected read_attribute method).
- alias [] read_attribute
-
- # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+.
- # (Alias for the protected write_attribute method).
- alias []= write_attribute
-
- public :[], :[]=
+ include ActiveRecord::Model
end
end
-require 'active_record/connection_adapters/abstract/connection_specification'
-ActiveSupport.run_load_hooks(:active_record, ActiveRecord::Base)
+ActiveSupport.run_load_hooks(:active_record, ActiveRecord::Model::DeprecationProxy)
diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb
index a175bf003c..8b2fc69b00 100644
--- a/activerecord/lib/active_record/callbacks.rb
+++ b/activerecord/lib/active_record/callbacks.rb
@@ -25,7 +25,7 @@ module ActiveRecord
# Check out <tt>ActiveRecord::Transactions</tt> for more details about <tt>after_commit</tt> and
# <tt>after_rollback</tt>.
#
- # Lastly an <tt>after_find</tt> and <tt>after_initialize</tt> callback is triggered for each object that
+ # Lastly an <tt>after_find</tt> and <tt>after_initialize</tt> callback is triggered for each object that
# is found and instantiated by a finder, with <tt>after_initialize</tt> being triggered after new objects
# are instantiated as well.
#
@@ -215,24 +215,48 @@ module ActiveRecord
# instead of quietly returning +false+.
#
# == Debugging callbacks
- #
- # The callback chain is accessible via the <tt>_*_callbacks</tt> method on an object. ActiveModel Callbacks support
+ #
+ # The callback chain is accessible via the <tt>_*_callbacks</tt> method on an object. ActiveModel Callbacks support
# <tt>:before</tt>, <tt>:after</tt> and <tt>:around</tt> as values for the <tt>kind</tt> property. The <tt>kind</tt> property
# defines what part of the chain the callback runs in.
- #
- # To find all callbacks in the before_save callback chain:
- #
+ #
+ # To find all callbacks in the before_save callback chain:
+ #
# Topic._save_callbacks.select { |cb| cb.kind.eql?(:before) }
- #
+ #
# Returns an array of callback objects that form the before_save chain.
- #
+ #
# To further check if the before_save chain contains a proc defined as <tt>rest_when_dead</tt> use the <tt>filter</tt> property of the callback object:
- #
+ #
# Topic._save_callbacks.select { |cb| cb.kind.eql?(:before) }.collect(&:filter).include?(:rest_when_dead)
- #
+ #
# Returns true or false depending on whether the proc is contained in the before_save callback chain on a Topic model.
- #
+ #
module Callbacks
+ # We can't define callbacks directly on ActiveRecord::Model because
+ # it is a module. So we queue up the definitions and execute them
+ # when ActiveRecord::Model is included.
+ module Register #:nodoc:
+ def self.extended(base)
+ base.config_attribute :_callbacks_register
+ base._callbacks_register = []
+ end
+
+ def self.setup(base)
+ base._callbacks_register.each do |item|
+ base.send(*item)
+ end
+ end
+
+ def define_callbacks(*args)
+ self._callbacks_register << [:define_callbacks, *args]
+ end
+
+ def define_model_callbacks(*args)
+ self._callbacks_register << [:define_model_callbacks, *args]
+ end
+ end
+
extend ActiveSupport::Concern
CALLBACKS = [
@@ -242,8 +266,11 @@ module ActiveRecord
:before_destroy, :around_destroy, :after_destroy, :after_commit, :after_rollback
]
+ module ClassMethods
+ include ActiveModel::Callbacks
+ end
+
included do
- extend ActiveModel::Callbacks
include ActiveModel::Validations::Callbacks
define_model_callbacks :initialize, :find, :touch, :only => :after
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
index 20863e73aa..b8f99adc22 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
@@ -1,7 +1,7 @@
require 'thread'
require 'monitor'
require 'set'
-require 'active_support/core_ext/module/synchronization'
+require 'active_support/core_ext/module/deprecation'
module ActiveRecord
# Raised when a connection could not be obtained within the connection
@@ -9,6 +9,13 @@ module ActiveRecord
class ConnectionTimeoutError < ConnectionNotEstablished
end
+ # Raised when a connection pool is full and another connection is requested
+ class PoolFullError < ConnectionNotEstablished
+ def initialize size, timeout
+ super("Connection pool of size #{size} and timeout #{timeout}s is full")
+ end
+ end
+
module ConnectionAdapters
# Connection pool base class for managing Active Record database
# connections.
@@ -57,10 +64,35 @@ module ActiveRecord
# * +wait_timeout+: number of seconds to block and wait for a connection
# before giving up and raising a timeout error (default 5 seconds).
class ConnectionPool
- attr_accessor :automatic_reconnect
- attr_reader :spec, :connections
- attr_reader :columns, :columns_hash, :primary_keys, :tables
- attr_reader :column_defaults
+ # Every +frequency+ seconds, the reaper will call +reap+ on +pool+.
+ # A reaper instantiated with a nil frequency will never reap the
+ # connection pool.
+ #
+ # Configure the frequency by setting "reaping_frequency" in your
+ # database yaml file.
+ class Reaper
+ attr_reader :pool, :frequency
+
+ def initialize(pool, frequency)
+ @pool = pool
+ @frequency = frequency
+ end
+
+ def run
+ return unless frequency
+ Thread.new(frequency, pool) { |t, p|
+ while true
+ sleep t
+ p.reap
+ end
+ }
+ end
+ end
+
+ include MonitorMixin
+
+ attr_accessor :automatic_reconnect, :timeout
+ attr_reader :spec, :connections, :size, :reaper
# Creates a new ConnectionPool object. +spec+ is a ConnectionSpecification
# object which describes database connection information (e.g. adapter,
@@ -69,88 +101,22 @@ module ActiveRecord
#
# The default ConnectionPool maximum size is 5.
def initialize(spec)
+ super()
+
@spec = spec
# The cache of reserved connections mapped to threads
@reserved_connections = {}
- # The mutex used to synchronize pool access
- @connection_mutex = Monitor.new
- @queue = @connection_mutex.new_cond
@timeout = spec.config[:wait_timeout] || 5
+ @reaper = Reaper.new self, spec.config[:reaping_frequency]
+ @reaper.run
# default max pool size to 5
@size = (spec.config[:pool] && spec.config[:pool].to_i) || 5
@connections = []
- @checked_out = []
@automatic_reconnect = true
- @tables = {}
- @visitor = nil
-
- @columns = Hash.new do |h, table_name|
- h[table_name] = with_connection do |conn|
-
- # Fetch a list of columns
- conn.columns(table_name, "#{table_name} Columns").tap do |columns|
-
- # set primary key information
- columns.each do |column|
- column.primary = column.name == primary_keys[table_name]
- end
- end
- end
- end
-
- @columns_hash = Hash.new do |h, table_name|
- h[table_name] = Hash[columns[table_name].map { |col|
- [col.name, col]
- }]
- end
-
- @column_defaults = Hash.new do |h, table_name|
- h[table_name] = Hash[columns[table_name].map { |col|
- [col.name, col.default]
- }]
- end
-
- @primary_keys = Hash.new do |h, table_name|
- h[table_name] = with_connection do |conn|
- table_exists?(table_name) ? conn.primary_key(table_name) : 'id'
- end
- end
- end
-
- # A cached lookup for table existence.
- def table_exists?(name)
- return true if @tables.key? name
-
- with_connection do |conn|
- conn.tables.each { |table| @tables[table] = true }
- @tables[name] = true if !@tables.key?(name) && conn.table_exists?(name)
- end
-
- @tables.key? name
- end
-
- # Clears out internal caches:
- #
- # * columns
- # * columns_hash
- # * tables
- def clear_cache!
- @columns.clear
- @columns_hash.clear
- @column_defaults.clear
- @tables.clear
- end
-
- # Clear out internal caches for table with +table_name+.
- def clear_table_cache!(table_name)
- @columns.delete table_name
- @columns_hash.delete table_name
- @column_defaults.delete table_name
- @primary_keys.delete table_name
end
# Retrieve the connection associated with the current thread, or call
@@ -165,7 +131,7 @@ module ActiveRecord
# Check to see if there is an active connection in this connection
# pool.
def active_connection?
- @reserved_connections.key? current_connection_id
+ active_connections.any?
end
# Signal that the thread is finished with the current connection.
@@ -181,7 +147,7 @@ module ActiveRecord
# connection when finished.
def with_connection
connection_id = current_connection_id
- fresh_connection = true unless @reserved_connections[connection_id]
+ fresh_connection = true unless active_connection?
yield connection
ensure
release_connection(connection_id) if fresh_connection
@@ -189,94 +155,80 @@ module ActiveRecord
# Returns true if a connection has already been opened.
def connected?
- !@connections.empty?
+ synchronize { @connections.any? }
end
# Disconnects all connections in the pool, and clears the pool.
def disconnect!
- @reserved_connections.each do |name,conn|
- checkin conn
- end
- @reserved_connections = {}
- @connections.each do |conn|
- conn.disconnect!
+ synchronize do
+ @reserved_connections = {}
+ @connections.each do |conn|
+ checkin conn
+ conn.disconnect!
+ end
+ @connections = []
end
- @connections = []
end
# Clears the cache which maps classes.
def clear_reloadable_connections!
- @reserved_connections.each do |name, conn|
- checkin conn
- end
- @reserved_connections = {}
- @connections.each do |conn|
- conn.disconnect! if conn.requires_reloading?
- end
- @connections.delete_if do |conn|
- conn.requires_reloading?
+ synchronize do
+ @reserved_connections = {}
+ @connections.each do |conn|
+ checkin conn
+ conn.disconnect! if conn.requires_reloading?
+ end
+ @connections.delete_if do |conn|
+ conn.requires_reloading?
+ end
end
end
# Verify active connections and remove and disconnect connections
# associated with stale threads.
def verify_active_connections! #:nodoc:
- clear_stale_cached_connections!
- @connections.each do |connection|
- connection.verify!
+ synchronize do
+ @connections.each do |connection|
+ connection.verify!
+ end
end
end
- # Return any checked-out connections back to the pool by threads that
- # are no longer alive.
- def clear_stale_cached_connections!
- keys = @reserved_connections.keys - Thread.list.find_all { |t|
- t.alive?
- }.map { |thread| thread.object_id }
- keys.each do |key|
- checkin @reserved_connections[key]
- @reserved_connections.delete(key)
- end
+ def clear_stale_cached_connections! # :nodoc:
end
+ deprecate :clear_stale_cached_connections!
# Check-out a database connection from the pool, indicating that you want
# to use it. You should call #checkin when you no longer need this.
#
- # This is done by either returning an existing connection, or by creating
- # a new connection. If the maximum number of connections for this pool has
- # already been reached, but the pool is empty (i.e. they're all being used),
- # then this method will wait until a thread has checked in a connection.
- # The wait time is bounded however: if no connection can be checked out
- # within the timeout specified for this pool, then a ConnectionTimeoutError
- # exception will be raised.
+ # This is done by either returning and leasing existing connection, or by
+ # creating a new connection and leasing it.
+ #
+ # If all connections are leased and the pool is at capacity (meaning the
+ # number of currently leased connections is greater than or equal to the
+ # size limit set), an ActiveRecord::PoolFullError exception will be raised.
#
# Returns: an AbstractAdapter object.
#
# Raises:
- # - ConnectionTimeoutError: no connection can be obtained from the pool
- # within the timeout period.
+ # - PoolFullError: no connection can be obtained from the pool.
def checkout
# Checkout an available connection
- @connection_mutex.synchronize do
- loop do
- conn = if @checked_out.size < @connections.size
- checkout_existing_connection
- elsif @connections.size < @size
- checkout_new_connection
- end
- return conn if conn
-
- @queue.wait(@timeout)
-
- if(@checked_out.size < @connections.size)
- next
- else
- clear_stale_cached_connections!
- if @size == @checked_out.size
- raise ConnectionTimeoutError, "could not obtain a database connection#{" within #{@timeout} seconds" if @timeout}. The max pool size is currently #{@size}; consider increasing it."
- end
- end
+ synchronize do
+ # Try to find a connection that hasn't been leased, and lease it
+ conn = connections.find { |c| c.lease }
+
+ # If all connections were leased, and we have room to expand,
+ # create a new connection and lease it.
+ if !conn && connections.size < size
+ conn = checkout_new_connection
+ conn.lease
+ end
+ if conn
+ checkout_and_verify conn
+ else
+ raise PoolFullError.new(size, timeout)
end
end
end
@@ -287,56 +239,69 @@ module ActiveRecord
# +conn+: an AbstractAdapter object, which was obtained by earlier by
# calling +checkout+ on this pool.
def checkin(conn)
- @connection_mutex.synchronize do
+ synchronize do
conn.run_callbacks :checkin do
- @checked_out.delete conn
- @queue.signal
+ conn.expire
end
end
end
- synchronize :clear_reloadable_connections!, :verify_active_connections!,
- :connected?, :disconnect!, :with => :@connection_mutex
+ # Remove a connection from the connection pool. The connection will
+ # remain open and active but will no longer be managed by this pool.
+ def remove(conn)
+ synchronize do
+ @connections.delete conn
- private
+ # FIXME: we might want to store the key on the connection so that removing
+ # from the reserved hash will be a little easier.
+ thread_id = @reserved_connections.keys.find { |k|
+ @reserved_connections[k] == conn
+ }
+ @reserved_connections.delete thread_id if thread_id
+ end
+ end
- def new_connection
- connection = ActiveRecord::Base.send(spec.adapter_method, spec.config)
+ # Removes dead connections from the pool. A dead connection can occur
+ # if a programmer forgets to close a connection at the end of a thread
+ # or a thread dies unexpectedly.
+ def reap
+ synchronize do
+ stale = Time.now - @timeout
+ connections.dup.each do |conn|
+ remove conn if conn.in_use? && stale > conn.last_use && !conn.active?
+ end
+ end
+ end
- # TODO: This is a bit icky, and in the long term we may want to change the method
- # signature for connections. Also, if we switch to have one visitor per
- # connection (and therefore per thread), we can get rid of the thread-local
- # variable in Arel::Visitors::ToSql.
- @visitor ||= connection.class.visitor_for(self)
- connection.visitor = @visitor
+ private
- connection
+ def new_connection
+ ActiveRecord::Base.send(spec.adapter_method, spec.config)
end
def current_connection_id #:nodoc:
- Thread.current.object_id
+ ActiveRecord::Base.connection_id ||= Thread.current.object_id
end
def checkout_new_connection
raise ConnectionNotEstablished unless @automatic_reconnect
c = new_connection
+ c.pool = self
@connections << c
- checkout_and_verify(c)
- end
-
- def checkout_existing_connection
- c = (@connections - @checked_out).first
- checkout_and_verify(c)
+ c
end
def checkout_and_verify(c)
c.run_callbacks :checkout do
c.verify!
- @checked_out << c
end
c
end
+
+ def active_connections
+ @connections.find_all { |c| c.in_use? }
+ end
end
# ConnectionHandler is a collection of ConnectionPool objects. It is used
@@ -367,10 +332,12 @@ module ActiveRecord
def initialize(pools = {})
@connection_pools = pools
+ @class_to_pool = {}
end
def establish_connection(name, spec)
- @connection_pools[name] = ConnectionAdapters::ConnectionPool.new(spec)
+ @connection_pools[spec] ||= ConnectionAdapters::ConnectionPool.new(spec)
+ @class_to_pool[name] = @connection_pools[spec]
end
# Returns true if there are any active connections among the connection
@@ -421,19 +388,20 @@ module ActiveRecord
# can be used as an argument for establish_connection, for easily
# re-establishing the connection.
def remove_connection(klass)
- pool = @connection_pools.delete(klass.name)
+ pool = @class_to_pool.delete(klass.name)
return nil unless pool
+ @connection_pools.delete pool.spec
pool.automatic_reconnect = false
pool.disconnect!
pool.spec.config
end
def retrieve_connection_pool(klass)
- pool = @connection_pools[klass.name]
+ pool = @class_to_pool[klass.name]
return pool if pool
- return nil if ActiveRecord::Base == klass
- retrieve_connection_pool klass.superclass
+ return nil if ActiveRecord::Model == klass
+ retrieve_connection_pool klass.active_record_super
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb
deleted file mode 100644
index c08c0263b9..0000000000
--- a/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb
+++ /dev/null
@@ -1,153 +0,0 @@
-module ActiveRecord
- class Base
- class ConnectionSpecification #:nodoc:
- attr_reader :config, :adapter_method
- def initialize (config, adapter_method)
- @config, @adapter_method = config, adapter_method
- end
- end
-
- ##
- # :singleton-method:
- # The connection handler
- class_attribute :connection_handler, :instance_writer => false
- self.connection_handler = ConnectionAdapters::ConnectionHandler.new
-
- # Returns the connection currently associated with the class. This can
- # also be used to "borrow" the connection to do database work that isn't
- # easily done without going straight to SQL.
- def connection
- self.class.connection
- end
-
- # Establishes the connection to the database. Accepts a hash as input where
- # the <tt>:adapter</tt> key must be specified with the name of a database adapter (in lower-case)
- # example for regular databases (MySQL, Postgresql, etc):
- #
- # ActiveRecord::Base.establish_connection(
- # :adapter => "mysql",
- # :host => "localhost",
- # :username => "myuser",
- # :password => "mypass",
- # :database => "somedatabase"
- # )
- #
- # Example for SQLite database:
- #
- # ActiveRecord::Base.establish_connection(
- # :adapter => "sqlite",
- # :database => "path/to/dbfile"
- # )
- #
- # Also accepts keys as strings (for parsing from YAML for example):
- #
- # ActiveRecord::Base.establish_connection(
- # "adapter" => "sqlite",
- # "database" => "path/to/dbfile"
- # )
- #
- # Or a URL:
- #
- # ActiveRecord::Base.establish_connection(
- # "postgres://myuser:mypass@localhost/somedatabase"
- # )
- #
- # The exceptions AdapterNotSpecified, AdapterNotFound and ArgumentError
- # may be returned on an error.
- def self.establish_connection(spec = ENV["DATABASE_URL"])
- case spec
- when nil
- raise AdapterNotSpecified unless defined?(Rails.env)
- establish_connection(Rails.env)
- when ConnectionSpecification
- self.connection_handler.establish_connection(name, spec)
- when Symbol, String
- if configuration = configurations[spec.to_s]
- establish_connection(configuration)
- elsif spec.is_a?(String) && hash = connection_url_to_hash(spec)
- establish_connection(hash)
- else
- raise AdapterNotSpecified, "#{spec} database is not configured"
- end
- else
- spec = spec.symbolize_keys
- unless spec.key?(:adapter) then raise AdapterNotSpecified, "database configuration does not specify adapter" end
-
- begin
- require "active_record/connection_adapters/#{spec[:adapter]}_adapter"
- rescue LoadError => e
- raise "Please install the #{spec[:adapter]} adapter: `gem install activerecord-#{spec[:adapter]}-adapter` (#{e})"
- end
-
- adapter_method = "#{spec[:adapter]}_connection"
- unless respond_to?(adapter_method)
- raise AdapterNotFound, "database configuration specifies nonexistent #{spec[:adapter]} adapter"
- end
-
- remove_connection
- establish_connection(ConnectionSpecification.new(spec, adapter_method))
- end
- end
-
- def self.connection_url_to_hash(url) # :nodoc:
- config = URI.parse url
- adapter = config.scheme
- adapter = "postgresql" if adapter == "postgres"
- spec = { :adapter => adapter,
- :username => config.user,
- :password => config.password,
- :port => config.port,
- :database => config.path.sub(%r{^/},""),
- :host => config.host }
- spec.reject!{ |_,value| !value }
- if config.query
- options = Hash[config.query.split("&").map{ |pair| pair.split("=") }].symbolize_keys
- spec.merge!(options)
- end
- spec
- end
-
- class << self
- # Returns the connection currently associated with the class. This can
- # also be used to "borrow" the connection to do database work unrelated
- # to any of the specific Active Records.
- def connection
- retrieve_connection
- end
-
- # Returns the configuration of the associated connection as a hash:
- #
- # ActiveRecord::Base.connection_config
- # # => {:pool=>5, :timeout=>5000, :database=>"db/development.sqlite3", :adapter=>"sqlite3"}
- #
- # Please use only for reading.
- def connection_config
- connection_pool.spec.config
- end
-
- def connection_pool
- connection_handler.retrieve_connection_pool(self) or raise ConnectionNotEstablished
- end
-
- def retrieve_connection
- connection_handler.retrieve_connection(self)
- end
-
- # Returns true if Active Record is connected.
- def connected?
- connection_handler.connected?(self)
- end
-
- def remove_connection(klass = self)
- connection_handler.remove_connection(klass)
- end
-
- def clear_active_connections!
- connection_handler.clear_active_connections!
- end
-
- delegate :clear_reloadable_connections!,
- :clear_all_connections!,:verify_active_connections!, :to => :connection_handler
- end
- end
-end
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 dc4a53034b..eb8cff9610 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
@@ -130,7 +130,7 @@ module ActiveRecord
#
# In order to get around this problem, #transaction will emulate the effect
# of nested transactions, by using savepoints:
- # http://dev.mysql.com/doc/refman/5.0/en/savepoints.html
+ # http://dev.mysql.com/doc/refman/5.0/en/savepoint.html
# Savepoints are supported by MySQL and PostgreSQL, but not SQLite3.
#
# It is safe to call this method if a database transaction is already open,
@@ -341,7 +341,7 @@ module ActiveRecord
# Send a rollback message to all records after they have been rolled back. If rollback
# is false, only rollback records since the last save point.
- def rollback_transaction_records(rollback) #:nodoc
+ def rollback_transaction_records(rollback)
if rollback
records = @_current_transaction_records.flatten
@_current_transaction_records.clear
@@ -361,7 +361,7 @@ module ActiveRecord
end
# Send a commit message to all records after they have been committed.
- def commit_transaction_records #:nodoc
+ def commit_transaction_records
records = @_current_transaction_records.flatten
@_current_transaction_records.clear
unless records.blank?
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
index 82f564e41d..132ca10f79 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
@@ -6,7 +6,7 @@ require 'bigdecimal/util'
module ActiveRecord
module ConnectionAdapters #:nodoc:
- class IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths) #:nodoc:
+ class IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths, :orders) #:nodoc:
end
# Abstract representation of a column definition. Instances of this type
@@ -46,13 +46,13 @@ module ActiveRecord
# +change_table+ is actually of this type:
#
# class SomeMigration < ActiveRecord::Migration
- # def self.up
+ # def up
# create_table :foo do |t|
# puts t.class # => "ActiveRecord::ConnectionAdapters::TableDefinition"
# end
# end
#
- # def self.down
+ # def down
# ...
# end
# end
@@ -66,6 +66,7 @@ module ActiveRecord
def initialize(base)
@columns = []
+ @columns_hash = {}
@base = base
end
@@ -86,7 +87,7 @@ module ActiveRecord
# Returns a ColumnDefinition for the column with name +name+.
def [](name)
- @columns.find {|column| column.name.to_s == name.to_s}
+ @columns_hash[name.to_s]
end
# Instantiates a new column for the table.
@@ -224,35 +225,38 @@ module ActiveRecord
# t.references :taggable, :polymorphic => { :default => 'Photo' }
# end
def column(name, type, options = {})
- column = self[name] || ColumnDefinition.new(@base, name, type)
- if options[:limit]
- column.limit = options[:limit]
- elsif native[type.to_sym].is_a?(Hash)
- column.limit = native[type.to_sym][:limit]
+ name = name.to_s
+ type = type.to_sym
+
+ column = self[name] || new_column_definition(@base, name, type)
+
+ limit = options.fetch(:limit) do
+ native[type][:limit] if native[type].is_a?(Hash)
end
+
+ column.limit = limit
column.precision = options[:precision]
- column.scale = options[:scale]
- column.default = options[:default]
- column.null = options[:null]
- @columns << column unless @columns.include? column
+ column.scale = options[:scale]
+ column.default = options[:default]
+ column.null = options[:null]
self
end
%w( string text integer float decimal datetime timestamp time date binary boolean ).each do |column_type|
class_eval <<-EOV, __FILE__, __LINE__ + 1
- def #{column_type}(*args) # def string(*args)
- options = args.extract_options! # options = args.extract_options!
- column_names = args # column_names = args
- #
- column_names.each { |name| column(name, '#{column_type}', options) } # column_names.each { |name| column(name, 'string', options) }
- end # end
+ def #{column_type}(*args) # def string(*args)
+ options = args.extract_options! # options = args.extract_options!
+ column_names = args # column_names = args
+ type = :'#{column_type}' # type = :string
+ column_names.each { |name| column(name, type, options) } # column_names.each { |name| column(name, type, options) }
+ end # end
EOV
end
# Appends <tt>:datetime</tt> columns <tt>:created_at</tt> and
# <tt>:updated_at</tt> to the table.
def timestamps(*args)
- options = args.extract_options!
+ options = { :null => false }.merge(args.extract_options!)
column(:created_at, :datetime, options)
column(:updated_at, :datetime, options)
end
@@ -275,9 +279,16 @@ module ActiveRecord
end
private
- def native
- @base.native_database_types
- end
+ def new_column_definition(base, name, type)
+ definition = ColumnDefinition.new base, name, type
+ @columns << definition
+ @columns_hash[name] = definition
+ definition
+ end
+
+ def native
+ @base.native_database_types
+ end
end
# Represents an SQL table in an abstract way for updating a table.
@@ -453,13 +464,13 @@ module ActiveRecord
def #{column_type}(*args) # def string(*args)
options = args.extract_options! # options = args.extract_options!
column_names = args # column_names = args
- #
+ type = :'#{column_type}' # type = :string
column_names.each do |name| # column_names.each do |name|
- column = ColumnDefinition.new(@base, name, '#{column_type}') # column = ColumnDefinition.new(@base, name, 'string')
+ column = ColumnDefinition.new(@base, name.to_s, type) # column = ColumnDefinition.new(@base, name, type)
if options[:limit] # if options[:limit]
column.limit = options[:limit] # column.limit = options[:limit]
- elsif native['#{column_type}'.to_sym].is_a?(Hash) # elsif native['string'.to_sym].is_a?(Hash)
- column.limit = native['#{column_type}'.to_sym][:limit] # column.limit = native['string'.to_sym][:limit]
+ elsif native[type].is_a?(Hash) # elsif native[type].is_a?(Hash)
+ column.limit = native[type][:limit] # column.limit = native[type][:limit]
end # end
column.precision = options[:precision] # column.precision = options[:precision]
column.scale = options[:scale] # column.scale = options[:scale]
@@ -479,4 +490,3 @@ module ActiveRecord
end
end
-
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
index 8e3ba1297e..20d3b4a1ef 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
@@ -1,4 +1,5 @@
require 'active_support/core_ext/array/wrap'
+require 'active_support/deprecation/reporting'
module ActiveRecord
module ConnectionAdapters # :nodoc:
@@ -15,8 +16,6 @@ module ActiveRecord
table_name[0...table_alias_length].gsub(/\./, '_')
end
- # def tables(name = nil) end
-
# Checks to see if the table +table_name+ exists on the database.
#
# === Example
@@ -113,7 +112,7 @@ module ActiveRecord
# Defaults to +id+. If <tt>:id</tt> is false this option is ignored.
#
# Also note that this just sets the primary key in the table. You additionally
- # need to configure the primary key in the model via the +set_primary_key+ macro.
+ # need to configure the primary key in the model via +self.primary_key=+.
# Models do NOT auto-detect the primary key from their table definition.
#
# [<tt>:options</tt>]
@@ -302,15 +301,8 @@ module ActiveRecord
# Adds a new index to the table. +column_name+ can be a single Symbol, or
# an Array of Symbols.
#
- # The index will be named after the table and the first column name,
- # unless you pass <tt>:name</tt> as an option.
- #
- # When creating an index on multiple columns, the first column is used as a name
- # for the index. For example, when you specify an index on two columns
- # [<tt>:first</tt>, <tt>:last</tt>], the DBMS creates an index for both columns as well as an
- # index for the first column <tt>:first</tt>. Using just the first name for this index
- # makes sense, because you will never have to create a singular index with this
- # name.
+ # The index will be named after the table and the column name(s), unless
+ # you pass <tt>:name</tt> as an option.
#
# ===== Examples
#
@@ -339,6 +331,14 @@ module ActiveRecord
# CREATE INDEX by_name_surname ON accounts(name(10), surname(15))
#
# Note: SQLite doesn't support index length
+ #
+ # ====== Creating an index with a sort order (desc or asc, asc is the default)
+ # add_index(:accounts, [:branch_id, :party_id, :surname], :order => {:branch_id => :desc, :part_id => :asc})
+ # generates
+ # CREATE INDEX by_branch_desc_party ON accounts(branch_id DESC, party_id ASC, surname)
+ #
+ # Note: mysql doesn't yet support index order (it accepts the syntax but ignores it)
+ #
def add_index(table_name, column_name, options = {})
index_name, index_type, index_columns = add_index_options(table_name, column_name, options)
execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} (#{index_columns})"
@@ -405,7 +405,7 @@ module ActiveRecord
def dump_schema_information #:nodoc:
sm_table = ActiveRecord::Migrator.schema_migrations_table_name
- migrated = select_values("SELECT version FROM #{sm_table}")
+ migrated = select_values("SELECT version FROM #{sm_table} ORDER BY version")
migrated.map { |v| "INSERT INTO #{sm_table} (version) VALUES ('#{v}');" }.join("\n\n")
end
@@ -426,6 +426,7 @@ module ActiveRecord
si_table = Base.table_name_prefix + 'schema_info' + Base.table_name_suffix
if table_exists?(si_table)
+ ActiveRecord::Deprecation.warn "Usage of the schema table `#{si_table}` is deprecated. Please switch to using `schema_migrations` table"
old_version = select_value("SELECT version FROM #{quote_table_name(si_table)}").to_i
assume_migrated_upto_version(old_version)
@@ -507,8 +508,8 @@ module ActiveRecord
# ===== Examples
# add_timestamps(:suppliers)
def add_timestamps(table_name)
- add_column table_name, :created_at, :datetime
- add_column table_name, :updated_at, :datetime
+ add_column table_name, :created_at, :datetime, :null => false
+ add_column table_name, :updated_at, :datetime, :null => false
end
# Removes the timestamp columns (created_at and updated_at) from the table definition.
@@ -520,9 +521,29 @@ module ActiveRecord
end
protected
+ def add_index_sort_order(option_strings, column_names, options = {})
+ if options.is_a?(Hash) && order = options[:order]
+ case order
+ when Hash
+ column_names.each {|name| option_strings[name] += " #{order[name].to_s.upcase}" if order.has_key?(name)}
+ when String
+ column_names.each {|name| option_strings[name] += " #{order.upcase}"}
+ end
+ end
+
+ return option_strings
+ end
+
# Overridden by the mysql adapter for supporting index lengths
def quoted_columns_for_index(column_names, options = {})
- column_names.map {|name| quote_column_name(name) }
+ option_strings = Hash[column_names.map {|name| [name, '']}]
+
+ # add index sort order if supported
+ if supports_index_sort_order?
+ option_strings = add_index_sort_order(option_strings, column_names, options)
+ end
+
+ column_names.map {|name| quote_column_name(name) + option_strings[name]}
end
def options_include_default?(options)
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
index 443e61b527..edea414db7 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -3,12 +3,15 @@ require 'bigdecimal'
require 'bigdecimal/util'
require 'active_support/core_ext/benchmark'
require 'active_support/deprecation'
+require 'active_record/connection_adapters/schema_cache'
+require 'monitor'
module ActiveRecord
module ConnectionAdapters # :nodoc:
extend ActiveSupport::Autoload
autoload :Column
+ autoload :ConnectionSpecification
autoload_under 'abstract' do
autoload :IndexDefinition, 'active_record/connection_adapters/abstract/schema_definitions'
@@ -24,7 +27,6 @@ module ActiveRecord
autoload :ConnectionPool
autoload :ConnectionHandler, 'active_record/connection_adapters/abstract/connection_pool'
autoload :ConnectionManagement, 'active_record/connection_adapters/abstract/connection_pool'
- autoload :ConnectionSpecification
autoload :QueryCache
end
@@ -47,39 +49,44 @@ module ActiveRecord
include DatabaseLimits
include QueryCache
include ActiveSupport::Callbacks
+ include MonitorMixin
define_callbacks :checkout, :checkin
- attr_accessor :visitor
-
- def initialize(connection, logger = nil) #:nodoc:
- @active = nil
- @connection, @logger = connection, logger
+ attr_accessor :visitor, :pool
+ attr_reader :schema_cache, :last_use, :in_use
+ alias :in_use? :in_use
+
+ def initialize(connection, logger = nil, pool = nil) #:nodoc:
+ super()
+
+ @active = nil
+ @connection = connection
+ @in_use = false
+ @instrumenter = ActiveSupport::Notifications.instrumenter
+ @last_use = false
+ @logger = logger
+ @open_transactions = 0
+ @pool = pool
+ @query_cache = Hash.new { |h,sql| h[sql] = {} }
@query_cache_enabled = false
- @query_cache = Hash.new { |h,sql| h[sql] = {} }
- @open_transactions = 0
- @instrumenter = ActiveSupport::Notifications.instrumenter
- @visitor = nil
- end
-
- # Returns a visitor instance for this adaptor, which conforms to the Arel::ToSql interface
- def self.visitor_for(pool) # :nodoc:
- adapter = pool.spec.config[:adapter]
-
- if Arel::Visitors::VISITORS[adapter]
- ActiveSupport::Deprecation.warn(
- "Arel::Visitors::VISITORS is deprecated and will be removed. Database adapters " \
- "should define a visitor_for method which returns the appropriate visitor for " \
- "the database. For example, MysqlAdapter.visitor_for(pool) returns " \
- "Arel::Visitors::MySQL.new(pool)."
- )
-
- Arel::Visitors::VISITORS[adapter].new(pool)
- else
- Arel::Visitors::ToSql.new(pool)
+ @schema_cache = SchemaCache.new self
+ @visitor = nil
+ end
+
+ def lease
+ synchronize do
+ unless in_use
+ @in_use = true
+ @last_use = Time.now
+ end
end
end
+ def expire
+ @in_use = false
+ end
+
# Returns the human-readable name of the adapter. Use mixed case - one
# can always use downcase if needed.
def adapter_name
@@ -130,6 +137,17 @@ module ActiveRecord
false
end
+ # Does this adapter support index sort order?
+ def supports_index_sort_order?
+ false
+ end
+
+ # Does this adapter support explain? As of this writing sqlite3,
+ # mysql2, and postgresql are the only ones that do.
+ def supports_explain?
+ false
+ end
+
# QUOTING ==================================================
# Override to return the quoted table name. Defaults to column quoting.
@@ -238,10 +256,19 @@ module ActiveRecord
node
end
+ def case_insensitive_comparison(table, attribute, column, value)
+ table[attribute].lower.eq(table.lower(value))
+ end
+
def current_savepoint_name
"active_record_#{open_transactions}"
end
+ # Check the connection back in to the connection pool
+ def close
+ pool.checkin self
+ end
+
protected
def log(sql, name = "SQL", binds = [])
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 4b7c74e0b8..560773ca86 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
@@ -4,6 +4,13 @@ module ActiveRecord
module ConnectionAdapters
class AbstractMysqlAdapter < AbstractAdapter
class Column < ConnectionAdapters::Column # :nodoc:
+ attr_reader :collation
+
+ def initialize(name, default, sql_type = nil, null = true, collation = nil)
+ super(name, default, sql_type, null)
+ @collation = collation
+ end
+
def extract_default(default)
if sql_type =~ /blob/i || type == :text
if default.blank?
@@ -28,6 +35,10 @@ module ActiveRecord
raise NotImplementedError
end
+ def case_sensitive?
+ collation && !collation.match(/_ci$/)
+ end
+
private
def simplified_type(field_type)
@@ -116,10 +127,7 @@ module ActiveRecord
super(connection, logger)
@connection_options, @config = connection_options, config
@quoted_column_names, @quoted_table_names = {}, {}
- end
-
- def self.visitor_for(pool) # :nodoc:
- Arel::Visitors::MySQL.new(pool)
+ @visitor = Arel::Visitors::MySQL.new self
end
def adapter_name #:nodoc:
@@ -144,6 +152,12 @@ module ActiveRecord
true
end
+ # Technically MySQL allows to create indexes with the sort order syntax
+ # but at the moment (5.5) it doesn't yet implement them
+ def supports_index_sort_order?
+ true
+ end
+
def native_database_types
NATIVE_DATABASE_TYPES
end
@@ -157,8 +171,8 @@ module ActiveRecord
end
# Overridden by the adapters to instantiate their specific Column type.
- def new_column(field, default, type, null) # :nodoc:
- Column.new(field, default, type, null)
+ def new_column(field, default, type, null, collation) # :nodoc:
+ Column.new(field, default, type, null, collation)
end
# Must return the Mysql error number from the exception, if the exception has an
@@ -298,11 +312,11 @@ module ActiveRecord
sql = "SHOW TABLES"
end
- select_all(sql).map do |table|
+ select_all(sql).map { |table|
table.delete('Table_type')
sql = "SHOW CREATE TABLE #{quote_table_name(table.to_a.first.last)}"
exec_without_stmt(sql).first['Create Table'] + ";\n\n"
- end.join("")
+ }.join
end
# Drops the database specified on the +name+ attribute
@@ -349,8 +363,10 @@ module ActiveRecord
show_variable 'collation_database'
end
- def tables(name = nil, database = nil) #:nodoc:
- sql = ["SHOW TABLES", database].compact.join(' IN ')
+ def tables(name = nil, database = nil, like = nil) #:nodoc:
+ sql = "SHOW TABLES "
+ sql << "IN #{database} " if database
+ sql << "LIKE #{quote(like)}" if like
execute_and_free(sql, 'SCHEMA') do |result|
result.collect { |field| field.first }
@@ -358,7 +374,8 @@ module ActiveRecord
end
def table_exists?(name)
- return true if super
+ return false unless name
+ return true if tables(nil, nil, name).any?
name = name.to_s
schema, table = name.split('.', 2)
@@ -368,7 +385,7 @@ module ActiveRecord
schema = nil
end
- tables(nil, schema).include? table
+ tables(nil, schema, table).any?
end
# Returns an array of indexes for the given table.
@@ -393,10 +410,10 @@ module ActiveRecord
# Returns an array of +Column+ objects for the table specified by +table_name+.
def columns(table_name, name = nil)#:nodoc:
- sql = "SHOW FIELDS FROM #{quote_table_name(table_name)}"
+ sql = "SHOW FULL FIELDS FROM #{quote_table_name(table_name)}"
execute_and_free(sql, 'SCHEMA') do |result|
each_hash(result).map do |field|
- new_column(field[:Field], field[:Default], field[:Type], field[:Null] == "YES")
+ new_column(field[:Field], field[:Default], field[:Type], field[:Null] == "YES", field[:Collation])
end
end
end
@@ -485,9 +502,14 @@ module ActiveRecord
# Returns a table's primary key and belonging sequence.
def pk_and_sequence_for(table)
- execute_and_free("DESCRIBE #{quote_table_name(table)}", 'SCHEMA') do |result|
- keys = each_hash(result).select { |row| row[:Key] == 'PRI' }.map { |row| row[:Field] }
- keys.length == 1 ? [keys.first, nil] : nil
+ execute_and_free("SHOW CREATE TABLE #{quote_table_name(table)}", 'SCHEMA') do |result|
+ create_table = each_hash(result).first[:"Create Table"]
+ if create_table.to_s =~ /PRIMARY KEY\s+\((.+)\)/
+ keys = $1.split(",").map { |key| key.gsub(/`/, "") }
+ keys.length == 1 ? [keys.first, nil] : nil
+ else
+ nil
+ end
end
end
@@ -501,23 +523,43 @@ module ActiveRecord
Arel::Nodes::Bin.new(node)
end
+ def case_insensitive_comparison(table, attribute, column, value)
+ if column.case_sensitive?
+ super
+ else
+ table[attribute].eq(value)
+ end
+ end
+
def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key)
where_sql
end
protected
+ def add_index_length(option_strings, column_names, options = {})
+ if options.is_a?(Hash) && length = options[:length]
+ case length
+ when Hash
+ column_names.each {|name| option_strings[name] += "(#{length[name]})" if length.has_key?(name)}
+ when Fixnum
+ column_names.each {|name| option_strings[name] += "(#{length})"}
+ end
+ end
+
+ return option_strings
+ end
+
def quoted_columns_for_index(column_names, options = {})
- length = options[:length] if options.is_a?(Hash)
+ option_strings = Hash[column_names.map {|name| [name, '']}]
- case length
- when Hash
- column_names.map {|name| length[name] ? "#{quote_column_name(name)}(#{length[name]})" : quote_column_name(name) }
- when Fixnum
- column_names.map {|name| "#{quote_column_name(name)}(#{length})"}
- else
- column_names.map {|name| quote_column_name(name) }
- end
+ # add index length
+ option_strings = add_index_length(option_strings, column_names, options)
+
+ # add index sort order
+ option_strings = add_index_sort_order(option_strings, column_names, options)
+
+ column_names.map {|name| quote_column_name(name) + option_strings[name]}
end
def translate_exception(exception, message)
diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb
index a7856539b7..6aa4a2c5b3 100644
--- a/activerecord/lib/active_record/connection_adapters/column.rb
+++ b/activerecord/lib/active_record/connection_adapters/column.rb
@@ -80,9 +80,10 @@ module ActiveRecord
when :decimal then klass.value_to_decimal(value)
when :datetime, :timestamp then klass.string_to_time(value)
when :time then klass.string_to_dummy_time(value)
- when :date then klass.string_to_date(value)
+ when :date then klass.value_to_date(value)
when :binary then klass.binary_to_string(value)
when :boolean then klass.value_to_boolean(value)
+ when :hstore then klass.cast_hstore(value)
else value
end
end
@@ -97,9 +98,10 @@ module ActiveRecord
when :decimal then "#{klass}.value_to_decimal(#{var_name})"
when :datetime, :timestamp then "#{klass}.string_to_time(#{var_name})"
when :time then "#{klass}.string_to_dummy_time(#{var_name})"
- when :date then "#{klass}.string_to_date(#{var_name})"
+ when :date then "#{klass}.value_to_date(#{var_name})"
when :binary then "#{klass}.binary_to_string(#{var_name})"
when :boolean then "#{klass}.value_to_boolean(#{var_name})"
+ when :hstore then "#{klass}.cast_hstore(#{var_name})"
else var_name
end
end
@@ -132,11 +134,15 @@ module ActiveRecord
value
end
- def string_to_date(string)
- return string unless string.is_a?(String)
- return nil if string.empty?
-
- fast_string_to_date(string) || fallback_string_to_date(string)
+ def value_to_date(value)
+ if value.is_a?(String)
+ return nil if value.empty?
+ fast_string_to_date(value) || fallback_string_to_date(value)
+ elsif value.respond_to?(:to_date)
+ value.to_date
+ else
+ value
+ end
end
def string_to_time(string)
diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb
new file mode 100644
index 0000000000..8491d42b86
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb
@@ -0,0 +1,83 @@
+module ActiveRecord
+ module ConnectionAdapters
+ class ConnectionSpecification #:nodoc:
+ attr_reader :config, :adapter_method
+
+ def initialize(config, adapter_method)
+ @config, @adapter_method = config, adapter_method
+ end
+
+ def initialize_dup(original)
+ @config = original.config.dup
+ end
+
+ ##
+ # Builds a ConnectionSpecification from user input
+ class Resolver # :nodoc:
+ attr_reader :config, :klass, :configurations
+
+ def initialize(config, configurations)
+ @config = config
+ @configurations = configurations
+ end
+
+ def spec
+ case config
+ when nil
+ raise AdapterNotSpecified unless defined?(Rails.env)
+ resolve_string_connection Rails.env
+ when Symbol, String
+ resolve_string_connection config.to_s
+ when Hash
+ resolve_hash_connection config
+ end
+ end
+
+ private
+ def resolve_string_connection(spec) # :nodoc:
+ hash = configurations.fetch(spec) do |k|
+ connection_url_to_hash(k)
+ end
+
+ raise(AdapterNotSpecified, "#{spec} database is not configured") unless hash
+
+ resolve_hash_connection hash
+ end
+
+ def resolve_hash_connection(spec) # :nodoc:
+ spec = spec.symbolize_keys
+
+ raise(AdapterNotSpecified, "database configuration does not specify adapter") unless spec.key?(:adapter)
+
+ begin
+ require "active_record/connection_adapters/#{spec[:adapter]}_adapter"
+ rescue LoadError => e
+ raise LoadError, "Please install the #{spec[:adapter]} adapter: `gem install activerecord-#{spec[:adapter]}-adapter` (#{e.message})", e.backtrace
+ end
+
+ adapter_method = "#{spec[:adapter]}_connection"
+
+ ConnectionSpecification.new(spec, adapter_method)
+ end
+
+ def connection_url_to_hash(url) # :nodoc:
+ config = URI.parse url
+ adapter = config.scheme
+ adapter = "postgresql" if adapter == "postgres"
+ spec = { :adapter => adapter,
+ :username => config.user,
+ :password => config.password,
+ :port => config.port,
+ :database => config.path.sub(%r{^/},""),
+ :host => config.host }
+ spec.reject!{ |_,value| !value }
+ if config.query
+ options = Hash[config.query.split("&").map{ |pair| pair.split("=") }].symbolize_keys
+ spec.merge!(options)
+ end
+ spec
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
index 8b574518e5..6086c32dbe 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
@@ -1,12 +1,12 @@
require 'active_record/connection_adapters/abstract_mysql_adapter'
-gem 'mysql2', '~> 0.3.6'
+gem 'mysql2', '~> 0.3.10'
require 'mysql2'
module ActiveRecord
- class Base
+ module ConnectionHandling
# Establishes a connection to the database that's used by all Active Record objects.
- def self.mysql2_connection(config)
+ def mysql2_connection(config)
config[:username] = 'root' if config[:username].nil?
if Mysql2::Client.const_defined? :FOUND_ROWS
@@ -35,6 +35,10 @@ module ActiveRecord
configure_connection
end
+ def supports_explain?
+ true
+ end
+
# HELPER METHODS ===========================================
def each_hash(result) # :nodoc:
@@ -47,8 +51,8 @@ module ActiveRecord
end
end
- def new_column(field, default, type, null) # :nodoc:
- Column.new(field, default, type, null)
+ def new_column(field, default, type, null, collation) # :nodoc:
+ Column.new(field, default, type, null, collation)
end
def error_number(exception)
@@ -93,6 +97,80 @@ module ActiveRecord
# DATABASE STATEMENTS ======================================
+ def explain(arel, binds = [])
+ sql = "EXPLAIN #{to_sql(arel)}"
+ start = Time.now
+ result = exec_query(sql, 'EXPLAIN', binds)
+ elapsed = Time.now - start
+
+ ExplainPrettyPrinter.new.pp(result, elapsed)
+ end
+
+ class ExplainPrettyPrinter # :nodoc:
+ # Pretty prints the result of a EXPLAIN in a way that resembles the output of the
+ # MySQL shell:
+ #
+ # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+
+ # | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+ # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+
+ # | 1 | SIMPLE | users | const | PRIMARY | PRIMARY | 4 | const | 1 | |
+ # | 1 | SIMPLE | posts | ALL | NULL | NULL | NULL | NULL | 1 | Using where |
+ # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+
+ # 2 rows in set (0.00 sec)
+ #
+ # This is an exercise in Ruby hyperrealism :).
+ def pp(result, elapsed)
+ widths = compute_column_widths(result)
+ separator = build_separator(widths)
+
+ pp = []
+
+ pp << separator
+ pp << build_cells(result.columns, widths)
+ pp << separator
+
+ result.rows.each do |row|
+ pp << build_cells(row, widths)
+ end
+
+ pp << separator
+ pp << build_footer(result.rows.length, elapsed)
+
+ pp.join("\n") + "\n"
+ end
+
+ private
+
+ def compute_column_widths(result)
+ [].tap do |widths|
+ result.columns.each_with_index do |column, i|
+ cells_in_column = [column] + result.rows.map {|r| r[i].nil? ? 'NULL' : r[i].to_s}
+ widths << cells_in_column.map(&:length).max
+ end
+ end
+ end
+
+ def build_separator(widths)
+ padding = 1
+ '+' + widths.map {|w| '-' * (w + (padding*2))}.join('+') + '+'
+ end
+
+ def build_cells(items, widths)
+ cells = []
+ items.each_with_index do |item, i|
+ item = 'NULL' if item.nil?
+ justifier = item.is_a?(Numeric) ? 'rjust' : 'ljust'
+ cells << item.to_s.send(justifier, widths[i])
+ end
+ '| ' + cells.join(' | ') + ' |'
+ end
+
+ def build_footer(nrows, elapsed)
+ rows_label = nrows == 1 ? 'row' : 'rows'
+ "#{nrows} #{rows_label} in set (%.2f sec)" % elapsed
+ end
+ end
+
# FIXME: re-enable the following once a "better" query_cache solution is in core
#
# The overrides below perform much better than the originals in AbstractAdapter
diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
index a1824fe396..e432c5af32 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
@@ -18,9 +18,9 @@ class Mysql
end
module ActiveRecord
- class Base
+ module ConnectionHandling
# Establishes a connection to the database that's used by all Active Record objects.
- def self.mysql_connection(config) # :nodoc:
+ def mysql_connection(config) # :nodoc:
config = config.symbolize_keys
host = config[:host]
port = config[:port]
@@ -150,8 +150,8 @@ module ActiveRecord
end
end
- def new_column(field, default, type, null) # :nodoc:
- Column.new(field, default, type, null)
+ def new_column(field, default, type, null, collation) # :nodoc:
+ Column.new(field, default, type, null, collation)
end
def error_number(exception) # :nodoc:
@@ -224,52 +224,48 @@ module ActiveRecord
@statements.clear
end
- if "<3".respond_to?(:encode)
- # Taken from here:
- # https://github.com/tmtm/ruby-mysql/blob/master/lib/mysql/charset.rb
- # Author: TOMITA Masahiro <tommy@tmtm.org>
- ENCODINGS = {
- "armscii8" => nil,
- "ascii" => Encoding::US_ASCII,
- "big5" => Encoding::Big5,
- "binary" => Encoding::ASCII_8BIT,
- "cp1250" => Encoding::Windows_1250,
- "cp1251" => Encoding::Windows_1251,
- "cp1256" => Encoding::Windows_1256,
- "cp1257" => Encoding::Windows_1257,
- "cp850" => Encoding::CP850,
- "cp852" => Encoding::CP852,
- "cp866" => Encoding::IBM866,
- "cp932" => Encoding::Windows_31J,
- "dec8" => nil,
- "eucjpms" => Encoding::EucJP_ms,
- "euckr" => Encoding::EUC_KR,
- "gb2312" => Encoding::EUC_CN,
- "gbk" => Encoding::GBK,
- "geostd8" => nil,
- "greek" => Encoding::ISO_8859_7,
- "hebrew" => Encoding::ISO_8859_8,
- "hp8" => nil,
- "keybcs2" => nil,
- "koi8r" => Encoding::KOI8_R,
- "koi8u" => Encoding::KOI8_U,
- "latin1" => Encoding::ISO_8859_1,
- "latin2" => Encoding::ISO_8859_2,
- "latin5" => Encoding::ISO_8859_9,
- "latin7" => Encoding::ISO_8859_13,
- "macce" => Encoding::MacCentEuro,
- "macroman" => Encoding::MacRoman,
- "sjis" => Encoding::SHIFT_JIS,
- "swe7" => nil,
- "tis620" => Encoding::TIS_620,
- "ucs2" => Encoding::UTF_16BE,
- "ujis" => Encoding::EucJP_ms,
- "utf8" => Encoding::UTF_8,
- "utf8mb4" => Encoding::UTF_8,
- }
- else
- ENCODINGS = Hash.new { |h,k| h[k] = k }
- end
+ # Taken from here:
+ # https://github.com/tmtm/ruby-mysql/blob/master/lib/mysql/charset.rb
+ # Author: TOMITA Masahiro <tommy@tmtm.org>
+ ENCODINGS = {
+ "armscii8" => nil,
+ "ascii" => Encoding::US_ASCII,
+ "big5" => Encoding::Big5,
+ "binary" => Encoding::ASCII_8BIT,
+ "cp1250" => Encoding::Windows_1250,
+ "cp1251" => Encoding::Windows_1251,
+ "cp1256" => Encoding::Windows_1256,
+ "cp1257" => Encoding::Windows_1257,
+ "cp850" => Encoding::CP850,
+ "cp852" => Encoding::CP852,
+ "cp866" => Encoding::IBM866,
+ "cp932" => Encoding::Windows_31J,
+ "dec8" => nil,
+ "eucjpms" => Encoding::EucJP_ms,
+ "euckr" => Encoding::EUC_KR,
+ "gb2312" => Encoding::EUC_CN,
+ "gbk" => Encoding::GBK,
+ "geostd8" => nil,
+ "greek" => Encoding::ISO_8859_7,
+ "hebrew" => Encoding::ISO_8859_8,
+ "hp8" => nil,
+ "keybcs2" => nil,
+ "koi8r" => Encoding::KOI8_R,
+ "koi8u" => Encoding::KOI8_U,
+ "latin1" => Encoding::ISO_8859_1,
+ "latin2" => Encoding::ISO_8859_2,
+ "latin5" => Encoding::ISO_8859_9,
+ "latin7" => Encoding::ISO_8859_13,
+ "macce" => Encoding::MacCentEuro,
+ "macroman" => Encoding::MacRoman,
+ "sjis" => Encoding::SHIFT_JIS,
+ "swe7" => nil,
+ "tis620" => Encoding::TIS_620,
+ "ucs2" => Encoding::UTF_16BE,
+ "ujis" => Encoding::EucJP_ms,
+ "utf8" => Encoding::UTF_8,
+ "utf8mb4" => Encoding::UTF_8,
+ }
# Get the client encoding for this database
def client_encoding
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
index 5402918b1d..d2361b5378 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -7,9 +7,9 @@ gem 'pg', '~> 0.11'
require 'pg'
module ActiveRecord
- class Base
+ module ConnectionHandling
# Establishes a connection to the database that's used by all Active Record objects
- def self.postgresql_connection(config) # :nodoc:
+ def postgresql_connection(config) # :nodoc:
config = config.symbolize_keys
host = config[:host]
port = config[:port] || 5432
@@ -49,6 +49,42 @@ module ActiveRecord
super
end
end
+
+ def cast_hstore(object)
+ if Hash === object
+ object.map { |k,v|
+ "#{escape_hstore(k)}=>#{escape_hstore(v)}"
+ }.join ', '
+ else
+ kvs = object.scan(/(?<!\\)".*?(?<!\\)"/).map { |o|
+ unescape_hstore(o[1...-1])
+ }
+ Hash[kvs.each_slice(2).to_a]
+ end
+ end
+
+ private
+ HSTORE_ESCAPE = {
+ ' ' => '\\ ',
+ '\\' => '\\\\',
+ '"' => '\\"',
+ '=' => '\\=',
+ }
+ HSTORE_ESCAPE_RE = Regexp.union(HSTORE_ESCAPE.keys)
+ HSTORE_UNESCAPE = HSTORE_ESCAPE.invert
+ HSTORE_UNESCAPE_RE = Regexp.union(HSTORE_UNESCAPE.keys)
+
+ def unescape_hstore(value)
+ value.gsub(HSTORE_UNESCAPE_RE) do |match|
+ HSTORE_UNESCAPE[match]
+ end
+ end
+
+ def escape_hstore(value)
+ value.gsub(HSTORE_ESCAPE_RE) do |match|
+ HSTORE_ESCAPE[match]
+ end
+ end
end
# :startdoc:
@@ -79,53 +115,55 @@ module ActiveRecord
# Maps PostgreSQL-specific data types to logical Rails types.
def simplified_type(field_type)
case field_type
- # Numeric and monetary types
- when /^(?:real|double precision)$/
- :float
- # Monetary types
- when 'money'
- :decimal
- # Character types
- when /^(?:character varying|bpchar)(?:\(\d+\))?$/
- :string
- # Binary data types
- when 'bytea'
- :binary
- # Date/time types
- when /^timestamp with(?:out)? time zone$/
- :datetime
- when 'interval'
- :string
- # Geometric types
- when /^(?:point|line|lseg|box|"?path"?|polygon|circle)$/
- :string
- # Network address types
- when /^(?:cidr|inet|macaddr)$/
- :string
- # Bit strings
- when /^bit(?: varying)?(?:\(\d+\))?$/
- :string
- # XML type
- when 'xml'
- :xml
- # tsvector type
- when 'tsvector'
- :tsvector
- # Arrays
- when /^\D+\[\]$/
- :string
- # Object identifier types
- when 'oid'
- :integer
- # UUID type
- when 'uuid'
- :string
- # Small and big integer types
- when /^(?:small|big)int$/
- :integer
- # Pass through all types that are not specific to PostgreSQL.
- else
- super
+ # Numeric and monetary types
+ when /^(?:real|double precision)$/
+ :float
+ # Monetary types
+ when 'money'
+ :decimal
+ when 'hstore'
+ :hstore
+ # Character types
+ when /^(?:character varying|bpchar)(?:\(\d+\))?$/
+ :string
+ # Binary data types
+ when 'bytea'
+ :binary
+ # Date/time types
+ when /^timestamp with(?:out)? time zone$/
+ :datetime
+ when 'interval'
+ :string
+ # Geometric types
+ when /^(?:point|line|lseg|box|"?path"?|polygon|circle)$/
+ :string
+ # Network address types
+ when /^(?:cidr|inet|macaddr)$/
+ :string
+ # Bit strings
+ when /^bit(?: varying)?(?:\(\d+\))?$/
+ :string
+ # XML type
+ when 'xml'
+ :xml
+ # tsvector type
+ when 'tsvector'
+ :tsvector
+ # Arrays
+ when /^\D+\[\]$/
+ :string
+ # Object identifier types
+ when 'oid'
+ :integer
+ # UUID type
+ when 'uuid'
+ :string
+ # Small and big integer types
+ when /^(?:small|big)int$/
+ :integer
+ # Pass through all types that are not specific to PostgreSQL.
+ else
+ super
end
end
@@ -215,6 +253,10 @@ module ActiveRecord
options = args.extract_options!
column(args[0], 'tsvector', options)
end
+
+ def hstore(name, options = {})
+ column(name, 'hstore', options)
+ end
end
ADAPTER_NAME = 'PostgreSQL'
@@ -247,6 +289,10 @@ module ActiveRecord
true
end
+ def supports_index_sort_order?
+ true
+ end
+
class StatementPool < ConnectionAdapters::StatementPool
def initialize(connection, max)
super
@@ -278,13 +324,24 @@ module ActiveRecord
cache.clear
end
+ def delete(sql_key)
+ dealloc cache[sql_key]
+ cache.delete sql_key
+ end
+
private
def cache
@cache[$$]
end
def dealloc(key)
- @connection.query "DEALLOCATE #{key}"
+ @connection.query "DEALLOCATE #{key}" if connection_active?
+ end
+
+ def connection_active?
+ @connection.status == PGconn::CONNECTION_OK
+ rescue PGError
+ false
end
end
@@ -292,6 +349,7 @@ module ActiveRecord
def initialize(connection, logger, connection_parameters, config)
super(connection, logger)
@connection_parameters, @config = connection_parameters, config
+ @visitor = Arel::Visitors::PostgreSQL.new self
# @local_tz is initialized as nil to avoid warnings when connect tries to use it
@local_tz = nil
@@ -308,10 +366,6 @@ module ActiveRecord
@local_tz = execute('SHOW TIME ZONE', 'SCHEMA').first["TimeZone"]
end
- def self.visitor_for(pool) # :nodoc:
- Arel::Visitors::PostgreSQL.new(pool)
- end
-
# Clears the prepared statements cache.
def clear_cache!
@statements.clear
@@ -378,6 +432,11 @@ module ActiveRecord
true
end
+ # Returns true.
+ def supports_explain?
+ true
+ end
+
# Returns the configured supported identifier length supported by PostgreSQL
def table_alias_length
@table_alias_length ||= query('SHOW max_identifier_length')[0][0].to_i
@@ -502,6 +561,48 @@ module ActiveRecord
# DATABASE STATEMENTS ======================================
+ def explain(arel, binds = [])
+ sql = "EXPLAIN #{to_sql(arel)}"
+ ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', binds))
+ end
+
+ class ExplainPrettyPrinter # :nodoc:
+ # Pretty prints the result of a EXPLAIN in a way that resembles the output of the
+ # PostgreSQL shell:
+ #
+ # QUERY PLAN
+ # ------------------------------------------------------------------------------
+ # Nested Loop Left Join (cost=0.00..37.24 rows=8 width=0)
+ # Join Filter: (posts.user_id = users.id)
+ # -> Index Scan using users_pkey on users (cost=0.00..8.27 rows=1 width=4)
+ # Index Cond: (id = 1)
+ # -> Seq Scan on posts (cost=0.00..28.88 rows=8 width=4)
+ # Filter: (posts.user_id = 1)
+ # (6 rows)
+ #
+ def pp(result)
+ header = result.columns.first
+ lines = result.rows.map(&:first)
+
+ # We add 2 because there's one char of padding at both sides, note
+ # the extra hyphens in the example above.
+ width = [header, *lines].map(&:length).max + 2
+
+ pp = []
+
+ pp << header.center(width).rstrip
+ pp << '-' * width
+
+ pp += lines.map {|line| " #{line}"}
+
+ nrows = result.rows.length
+ rows_label = nrows == 1 ? 'row' : 'rows'
+ pp << "(#{nrows} #{rows_label})"
+
+ pp.join("\n") + "\n"
+ end
+ end
+
# Executes a SELECT query and returns an array of rows. Each row is an
# array of field values.
def select_rows(sql, name = nil)
@@ -743,16 +844,15 @@ module ActiveRecord
# Returns an array of indexes for the given table.
def indexes(table_name, name = nil)
- schemas = schema_search_path.split(/,/).map { |p| quote(p) }.join(',')
result = query(<<-SQL, name)
- SELECT distinct i.relname, d.indisunique, d.indkey, t.oid
+ SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid
FROM pg_class t
INNER JOIN pg_index d ON t.oid = d.indrelid
INNER JOIN pg_class i ON d.indexrelid = i.oid
WHERE i.relkind = 'i'
AND d.indisprimary = 'f'
AND t.relname = '#{table_name}'
- AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname IN (#{schemas}) )
+ AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = ANY (current_schemas(false)) )
ORDER BY i.relname
SQL
@@ -761,7 +861,8 @@ module ActiveRecord
index_name = row[0]
unique = row[1] == 't'
indkey = row[2].split(" ")
- oid = row[3]
+ inddef = row[3]
+ oid = row[4]
columns = Hash[query(<<-SQL, "Columns for index #{row[0]} on #{table_name}")]
SELECT a.attnum, a.attname
@@ -771,7 +872,12 @@ module ActiveRecord
SQL
column_names = columns.values_at(*indkey).compact
- column_names.empty? ? nil : IndexDefinition.new(table_name, index_name, unique, column_names)
+
+ # add info on sort order for columns (only desc order is explicitly specified, asc is the default)
+ desc_order_columns = inddef.scan(/(\w+) DESC/).flatten
+ orders = desc_order_columns.any? ? Hash[desc_order_columns.map {|order_column| [order_column, :desc]}] : {}
+
+ column_names.empty? ? nil : IndexDefinition.new(table_name, index_name, unique, column_names, [], orders)
end.compact
end
@@ -815,7 +921,7 @@ module ActiveRecord
# Returns the active schema search path.
def schema_search_path
- @schema_search_path ||= query('SHOW search_path')[0][0]
+ @schema_search_path ||= query('SHOW search_path', 'SCHEMA')[0][0]
end
# Returns the current client message level.
@@ -911,12 +1017,14 @@ module ActiveRecord
# Example:
# rename_table('octopuses', 'octopi')
def rename_table(name, new_name)
+ clear_cache!
execute "ALTER TABLE #{quote_table_name(name)} RENAME TO #{quote_table_name(new_name)}"
end
# Adds a new column to the named table.
# See TableDefinition#column for details of the options you can use.
def add_column(table_name, column_name, type, options = {})
+ clear_cache!
add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ADD COLUMN #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
add_column_options!(add_column_sql, options)
@@ -925,6 +1033,7 @@ module ActiveRecord
# Changes the column of a table.
def change_column(table_name, column_name, type, options = {})
+ clear_cache!
quoted_table_name = quote_table_name(table_name)
execute "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
@@ -935,10 +1044,12 @@ module ActiveRecord
# Changes the default value of a table column.
def change_column_default(table_name, column_name, default)
+ clear_cache!
execute "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} SET DEFAULT #{quote(default)}"
end
def change_column_null(table_name, column_name, null, default = nil)
+ clear_cache!
unless null || default.nil?
execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
end
@@ -947,6 +1058,7 @@ module ActiveRecord
# Renames a column in a table.
def rename_column(table_name, column_name, new_column_name)
+ clear_cache!
execute "ALTER TABLE #{quote_table_name(table_name)} RENAME COLUMN #{quote_column_name(column_name)} TO #{quote_column_name(new_column_name)}"
end
@@ -1030,26 +1142,54 @@ module ActiveRecord
end
private
+ FEATURE_NOT_SUPPORTED = "0A000" # :nodoc:
+
def exec_no_cache(sql, binds)
@connection.async_exec(sql)
end
def exec_cache(sql, binds)
- unless @statements.key? sql
- nextkey = @statements.next_key
- @connection.prepare nextkey, sql
- @statements[sql] = nextkey
+ begin
+ stmt_key = prepare_statement sql
+
+ # Clear the queue
+ @connection.get_last_result
+ @connection.send_query_prepared(stmt_key, binds.map { |col, val|
+ type_cast(val, col)
+ })
+ @connection.block
+ @connection.get_last_result
+ rescue PGError => e
+ # Get the PG code for the failure. Annoyingly, the code for
+ # prepared statements whose return value may have changed is
+ # FEATURE_NOT_SUPPORTED. Check here for more details:
+ # http://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/utils/cache/plancache.c#l573
+ code = e.result.result_error_field(PGresult::PG_DIAG_SQLSTATE)
+ if FEATURE_NOT_SUPPORTED == code
+ @statements.delete sql_key(sql)
+ retry
+ else
+ raise e
+ end
end
+ end
- key = @statements[sql]
+ # Returns the statement identifier for the client side cache
+ # of statements
+ def sql_key(sql)
+ "#{schema_search_path}-#{sql}"
+ end
- # Clear the queue
- @connection.get_last_result
- @connection.send_query_prepared(key, binds.map { |col, val|
- type_cast(val, col)
- })
- @connection.block
- @connection.get_last_result
+ # Prepare the statement if it hasn't been prepared, return
+ # the statement key.
+ def prepare_statement(sql)
+ sql_key = sql_key(sql)
+ unless @statements.key? sql_key
+ nextkey = @statements.next_key
+ @connection.prepare nextkey, sql
+ @statements[sql_key] = nextkey
+ end
+ @statements[sql_key]
end
# The internal PostgreSQL identifier of the money data type.
diff --git a/activerecord/lib/active_record/connection_adapters/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb
new file mode 100644
index 0000000000..4e8932a695
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb
@@ -0,0 +1,50 @@
+module ActiveRecord
+ module ConnectionAdapters
+ class SchemaCache
+ attr_reader :columns, :columns_hash, :primary_keys, :tables
+ attr_reader :connection
+
+ def initialize(conn)
+ @connection = conn
+ @tables = {}
+
+ @columns = Hash.new do |h, table_name|
+ h[table_name] = conn.columns(table_name, "#{table_name} Columns")
+ end
+
+ @columns_hash = Hash.new do |h, table_name|
+ h[table_name] = Hash[columns[table_name].map { |col|
+ [col.name, col]
+ }]
+ end
+
+ @primary_keys = Hash.new do |h, table_name|
+ h[table_name] = table_exists?(table_name) ? conn.primary_key(table_name) : nil
+ end
+ end
+
+ # A cached lookup for table existence.
+ def table_exists?(name)
+ return @tables[name] if @tables.key? name
+
+ @tables[name] = connection.table_exists?(name)
+ end
+
+ # Clears out internal caches
+ def clear!
+ @columns.clear
+ @columns_hash.clear
+ @primary_keys.clear
+ @tables.clear
+ end
+
+ # Clear out internal caches for table with +table_name+.
+ def clear_table_cache!(table_name)
+ @columns.delete table_name
+ @columns_hash.delete table_name
+ @primary_keys.delete table_name
+ @tables.delete table_name
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
index 0a0da0b5d3..ee5d10859c 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
@@ -1,12 +1,12 @@
require 'active_record/connection_adapters/sqlite_adapter'
-gem 'sqlite3', '~> 1.3.4'
+gem 'sqlite3', '~> 1.3.5'
require 'sqlite3'
module ActiveRecord
- class Base
+ module ConnectionHandling
# sqlite3 adapter reuses sqlite_connection.
- def self.sqlite3_connection(config) # :nodoc:
+ def sqlite3_connection(config) # :nodoc:
# Require database.
unless config[:database]
raise ArgumentError, "No database file specified. Missing argument: database"
@@ -47,11 +47,7 @@ module ActiveRecord
# Returns the current database encoding format as a string, eg: 'UTF-8'
def encoding
- if @connection.respond_to?(:encoding)
- @connection.encoding.to_s
- else
- @connection.execute('PRAGMA encoding')[0]['encoding']
- end
+ @connection.encoding.to_s
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
index 1932a849ee..69750a911d 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
@@ -16,7 +16,7 @@ module ActiveRecord
end
def binary_to_string(value)
- if value.respond_to?(:force_encoding) && value.encoding != Encoding::ASCII_8BIT
+ if value.encoding != Encoding::ASCII_8BIT
value = value.force_encoding(Encoding::ASCII_8BIT)
end
@@ -89,10 +89,7 @@ module ActiveRecord
@statements = StatementPool.new(@connection,
config.fetch(:statement_limit) { 1000 })
@config = config
- end
-
- def self.visitor_for(pool) # :nodoc:
- Arel::Visitors::SQLite.new(pool)
+ @visitor = Arel::Visitors::SQLite.new self
end
def adapter_name #:nodoc:
@@ -125,6 +122,11 @@ module ActiveRecord
true
end
+ # Returns true.
+ def supports_explain?
+ true
+ end
+
def requires_reloading?
true
end
@@ -157,6 +159,10 @@ module ActiveRecord
sqlite_version >= '3.1.0'
end
+ def supports_index_sort_order?
+ sqlite_version >= '3.3.0'
+ end
+
def native_database_types #:nodoc:
{
:primary_key => default_primary_key_type,
@@ -195,29 +201,40 @@ module ActiveRecord
end
end
- if "<3".encoding_aware?
- def type_cast(value, column) # :nodoc:
- return value.to_f if BigDecimal === value
- return super unless String === value
- return super unless column && value
-
- value = super
- if column.type == :string && value.encoding == Encoding::ASCII_8BIT
- @logger.error "Binary data inserted for `string` type on column `#{column.name}`"
- value.encode! 'utf-8'
- end
- value
- end
- else
- def type_cast(value, column) # :nodoc:
- return super unless BigDecimal === value
+ def type_cast(value, column) # :nodoc:
+ return value.to_f if BigDecimal === value
+ return super unless String === value
+ return super unless column && value
- value.to_f
+ value = super
+ if column.type == :string && value.encoding == Encoding::ASCII_8BIT
+ @logger.error "Binary data inserted for `string` type on column `#{column.name}`"
+ value.encode! 'utf-8'
end
+ value
end
# DATABASE STATEMENTS ======================================
+ def explain(arel, binds = [])
+ sql = "EXPLAIN QUERY PLAN #{to_sql(arel)}"
+ ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', binds))
+ end
+
+ class ExplainPrettyPrinter
+ # Pretty prints the result of a EXPLAIN QUERY PLAN in a way that resembles
+ # the output of the SQLite shell:
+ #
+ # 0|0|0|SEARCH TABLE users USING INTEGER PRIMARY KEY (rowid=?) (~1 rows)
+ # 0|1|1|SCAN TABLE posts (~100000 rows)
+ #
+ def pp(result) # :nodoc:
+ result.rows.map do |row|
+ row.join('|')
+ end.join("\n") + "\n"
+ end
+ end
+
def exec_query(sql, name = nil, binds = [])
log(sql, name, binds) do
@@ -304,18 +321,23 @@ module ActiveRecord
# SCHEMA STATEMENTS ========================================
- def tables(name = 'SCHEMA') #:nodoc:
+ def tables(name = 'SCHEMA', table_name = nil) #:nodoc:
sql = <<-SQL
SELECT name
FROM sqlite_master
WHERE type = 'table' AND NOT name = 'sqlite_sequence'
SQL
+ sql << " AND name = #{quote_table_name(table_name)}" if table_name
exec_query(sql, name).map do |row|
row['name']
end
end
+ def table_exists?(name)
+ name && tables('SCHEMA', name).any?
+ end
+
# Returns an array of +SQLiteColumn+ objects for the table specified by +table_name+.
def columns(table_name, name = nil) #:nodoc:
table_structure(table_name).map do |field|
@@ -413,6 +435,8 @@ module ActiveRecord
self.limit = options[:limit] if options.include?(:limit)
self.default = options[:default] if include_default
self.null = options[:null] if options.include?(:null)
+ self.precision = options[:precision] if options.include?(:precision)
+ self.scale = options[:scale] if options.include?(:scale)
end
end
end
@@ -467,6 +491,7 @@ module ActiveRecord
@definition.column(column_name, column.type,
:limit => column.limit, :default => column.default,
+ :precision => column.precision, :scale => column.scale,
:null => column.null)
end
@definition.primary_key(primary_key(from)) if primary_key(from)
diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb
new file mode 100644
index 0000000000..e9cd3d5e57
--- /dev/null
+++ b/activerecord/lib/active_record/connection_handling.rb
@@ -0,0 +1,100 @@
+require 'active_support/core_ext/module/delegation'
+
+module ActiveRecord
+ module ConnectionHandling
+ # Establishes the connection to the database. Accepts a hash as input where
+ # the <tt>:adapter</tt> key must be specified with the name of a database adapter (in lower-case)
+ # example for regular databases (MySQL, Postgresql, etc):
+ #
+ # ActiveRecord::Base.establish_connection(
+ # :adapter => "mysql",
+ # :host => "localhost",
+ # :username => "myuser",
+ # :password => "mypass",
+ # :database => "somedatabase"
+ # )
+ #
+ # Example for SQLite database:
+ #
+ # ActiveRecord::Base.establish_connection(
+ # :adapter => "sqlite",
+ # :database => "path/to/dbfile"
+ # )
+ #
+ # Also accepts keys as strings (for parsing from YAML for example):
+ #
+ # ActiveRecord::Base.establish_connection(
+ # "adapter" => "sqlite",
+ # "database" => "path/to/dbfile"
+ # )
+ #
+ # Or a URL:
+ #
+ # ActiveRecord::Base.establish_connection(
+ # "postgres://myuser:mypass@localhost/somedatabase"
+ # )
+ #
+ # The exceptions AdapterNotSpecified, AdapterNotFound and ArgumentError
+ # may be returned on an error.
+ def establish_connection(spec = ENV["DATABASE_URL"])
+ resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new spec, configurations
+ spec = resolver.spec
+
+ unless respond_to?(spec.adapter_method)
+ raise AdapterNotFound, "database configuration specifies nonexistent #{spec.config[:adapter]} adapter"
+ end
+
+ remove_connection
+ connection_handler.establish_connection name, spec
+ end
+
+ # Returns the connection currently associated with the class. This can
+ # also be used to "borrow" the connection to do database work unrelated
+ # to any of the specific Active Records.
+ def connection
+ retrieve_connection
+ end
+
+ def connection_id
+ Thread.current['ActiveRecord::Base.connection_id']
+ end
+
+ def connection_id=(connection_id)
+ Thread.current['ActiveRecord::Base.connection_id'] = connection_id
+ end
+
+ # Returns the configuration of the associated connection as a hash:
+ #
+ # ActiveRecord::Base.connection_config
+ # # => {:pool=>5, :timeout=>5000, :database=>"db/development.sqlite3", :adapter=>"sqlite3"}
+ #
+ # Please use only for reading.
+ def connection_config
+ connection_pool.spec.config
+ end
+
+ def connection_pool
+ connection_handler.retrieve_connection_pool(self) or raise ConnectionNotEstablished
+ end
+
+ def retrieve_connection
+ connection_handler.retrieve_connection(self)
+ end
+
+ # Returns true if Active Record is connected.
+ def connected?
+ connection_handler.connected?(self)
+ end
+
+ def remove_connection(klass = self)
+ connection_handler.remove_connection(klass)
+ end
+
+ def clear_active_connections!
+ connection_handler.clear_active_connections!
+ end
+
+ delegate :clear_reloadable_connections!,
+ :clear_all_connections!,:verify_active_connections!, :to => :connection_handler
+ end
+end
diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb
new file mode 100644
index 0000000000..980f8fe50f
--- /dev/null
+++ b/activerecord/lib/active_record/core.rb
@@ -0,0 +1,354 @@
+require 'active_support/concern'
+
+module ActiveRecord
+ module Core
+ extend ActiveSupport::Concern
+
+ included do
+ ##
+ # :singleton-method:
+ # Accepts a logger conforming to the interface of Log4r or the default Ruby 1.8+ Logger class,
+ # which is then passed on to any new database connections made and which can be retrieved on both
+ # a class and instance level by calling +logger+.
+ config_attribute :logger, :global => true
+
+ ##
+ # :singleton-method:
+ # Contains the database configuration - as is typically stored in config/database.yml -
+ # as a Hash.
+ #
+ # For example, the following database.yml...
+ #
+ # development:
+ # adapter: sqlite3
+ # database: db/development.sqlite3
+ #
+ # production:
+ # adapter: sqlite3
+ # database: db/production.sqlite3
+ #
+ # ...would result in ActiveRecord::Base.configurations to look like this:
+ #
+ # {
+ # 'development' => {
+ # 'adapter' => 'sqlite3',
+ # 'database' => 'db/development.sqlite3'
+ # },
+ # 'production' => {
+ # 'adapter' => 'sqlite3',
+ # 'database' => 'db/production.sqlite3'
+ # }
+ # }
+ config_attribute :configurations, :global => true
+ self.configurations = {}
+
+ ##
+ # :singleton-method:
+ # Determines whether to use Time.local (using :local) or Time.utc (using :utc) when pulling
+ # dates and times from the database. This is set to :local by default.
+ config_attribute :default_timezone, :global => true
+ self.default_timezone = :local
+
+ ##
+ # :singleton-method:
+ # Specifies the format to use when dumping the database schema with Rails'
+ # Rakefile. If :sql, the schema is dumped as (potentially database-
+ # specific) SQL statements. If :ruby, the schema is dumped as an
+ # ActiveRecord::Schema file which can be loaded into any database that
+ # supports migrations. Use :ruby if you want to have different database
+ # adapters for, e.g., your development and test environments.
+ config_attribute :schema_format, :global => true
+ self.schema_format = :ruby
+
+ ##
+ # :singleton-method:
+ # Specify whether or not to use timestamps for migration versions
+ config_attribute :timestamped_migrations, :global => true
+ self.timestamped_migrations = true
+
+ ##
+ # :singleton-method:
+ # The connection handler
+ config_attribute :connection_handler
+ self.connection_handler = ConnectionAdapters::ConnectionHandler.new
+ end
+
+ module ClassMethods
+ def inherited(child_class) #:nodoc:
+ child_class.initialize_generated_modules
+ super
+ end
+
+ def initialize_generated_modules
+ # force attribute methods to be higher in inheritance hierarchy than other generated methods
+ generated_attribute_methods
+ generated_feature_methods
+ end
+
+ def generated_feature_methods
+ @generated_feature_methods ||= begin
+ mod = const_set(:GeneratedFeatureMethods, Module.new)
+ include mod
+ mod
+ end
+ end
+
+ # Returns a string like 'Post(id:integer, title:string, body:text)'
+ def inspect
+ if self == Base
+ super
+ elsif abstract_class?
+ "#{super}(abstract)"
+ elsif table_exists?
+ attr_list = columns.map { |c| "#{c.name}: #{c.type}" } * ', '
+ "#{super}(#{attr_list})"
+ else
+ "#{super}(Table doesn't exist)"
+ end
+ end
+
+ # Overwrite the default class equality method to provide support for association proxies.
+ def ===(object)
+ object.is_a?(self)
+ end
+
+ def arel_table
+ @arel_table ||= Arel::Table.new(table_name, arel_engine)
+ end
+
+ def arel_engine
+ @arel_engine ||= connection_handler.connection_pools[name] ? self : active_record_super.arel_engine
+ end
+
+ private
+
+ def relation #:nodoc:
+ @relation ||= Relation.new(self, arel_table)
+
+ if finder_needs_type_condition?
+ @relation.where(type_condition).create_with(inheritance_column.to_sym => sti_name)
+ else
+ @relation
+ end
+ end
+ end
+
+ # New objects can be instantiated as either empty (pass no construction parameter) or pre-set with
+ # attributes but not yet saved (pass a hash with key names matching the associated table column names).
+ # In both instances, valid attribute keys are determined by the column names of the associated table --
+ # hence you can't have attributes that aren't part of the table columns.
+ #
+ # +initialize+ respects mass-assignment security and accepts either +:as+ or +:without_protection+ options
+ # in the +options+ parameter.
+ #
+ # ==== Examples
+ # # Instantiates a single new object
+ # User.new(:first_name => 'Jamie')
+ #
+ # # Instantiates a single new object using the :admin mass-assignment security role
+ # User.new({ :first_name => 'Jamie', :is_admin => true }, :as => :admin)
+ #
+ # # Instantiates a single new object bypassing mass-assignment security
+ # User.new({ :first_name => 'Jamie', :is_admin => true }, :without_protection => true)
+ def initialize(attributes = nil, options = {})
+ @attributes = self.class.initialize_attributes(self.class.column_defaults.dup)
+ @association_cache = {}
+ @aggregation_cache = {}
+ @attributes_cache = {}
+ @new_record = true
+ @readonly = false
+ @destroyed = false
+ @marked_for_destruction = false
+ @previously_changed = {}
+ @changed_attributes = {}
+ @relation = nil
+
+ ensure_proper_type
+
+ populate_with_current_scope_attributes
+
+ assign_attributes(attributes, options) if attributes
+
+ yield self if block_given?
+ run_callbacks :initialize
+ end
+
+ # Initialize an empty model object from +coder+. +coder+ must contain
+ # the attributes necessary for initializing an empty model object. For
+ # example:
+ #
+ # class Post < ActiveRecord::Base
+ # end
+ #
+ # post = Post.allocate
+ # post.init_with('attributes' => { 'title' => 'hello world' })
+ # post.title # => 'hello world'
+ def init_with(coder)
+ @attributes = self.class.initialize_attributes(coder['attributes'])
+ @relation = nil
+
+ @attributes_cache, @previously_changed, @changed_attributes = {}, {}, {}
+ @association_cache = {}
+ @aggregation_cache = {}
+ @readonly = @destroyed = @marked_for_destruction = false
+ @new_record = false
+ run_callbacks :find
+ run_callbacks :initialize
+
+ self
+ end
+
+ # Duped objects have no id assigned and are treated as new records. Note
+ # that this is a "shallow" copy as it copies the object's attributes
+ # only, not its associations. The extent of a "deep" copy is application
+ # specific and is therefore left to the application to implement according
+ # to its need.
+ # The dup method does not preserve the timestamps (created|updated)_(at|on).
+ def initialize_dup(other)
+ cloned_attributes = other.clone_attributes(:read_attribute_before_type_cast)
+ cloned_attributes.delete(self.class.primary_key)
+
+ @attributes = cloned_attributes
+
+ _run_after_initialize_callbacks if respond_to?(:_run_after_initialize_callbacks)
+
+ @changed_attributes = {}
+ self.class.column_defaults.each do |attr, orig_value|
+ @changed_attributes[attr] = orig_value if field_changed?(attr, orig_value, @attributes[attr])
+ end
+
+ @aggregation_cache = {}
+ @association_cache = {}
+ @attributes_cache = {}
+ @new_record = true
+
+ ensure_proper_type
+ populate_with_current_scope_attributes
+ super
+ end
+
+ # Populate +coder+ with attributes about this record that should be
+ # serialized. The structure of +coder+ defined in this method is
+ # guaranteed to match the structure of +coder+ passed to the +init_with+
+ # method.
+ #
+ # Example:
+ #
+ # class Post < ActiveRecord::Base
+ # end
+ # coder = {}
+ # Post.new.encode_with(coder)
+ # coder # => { 'id' => nil, ... }
+ def encode_with(coder)
+ coder['attributes'] = attributes
+ end
+
+ # Returns true if +comparison_object+ is the same exact object, or +comparison_object+
+ # is of the same type and +self+ has an ID and it is equal to +comparison_object.id+.
+ #
+ # Note that new records are different from any other record by definition, unless the
+ # other record is the receiver itself. Besides, if you fetch existing records with
+ # +select+ and leave the ID out, you're on your own, this predicate will return false.
+ #
+ # Note also that destroying a record preserves its ID in the model instance, so deleted
+ # models are still comparable.
+ def ==(comparison_object)
+ super ||
+ comparison_object.instance_of?(self.class) &&
+ id.present? &&
+ comparison_object.id == id
+ end
+ alias :eql? :==
+
+ # Delegates to id in order to allow two records of the same type and id to work with something like:
+ # [ Person.find(1), Person.find(2), Person.find(3) ] & [ Person.find(1), Person.find(4) ] # => [ Person.find(1) ]
+ def hash
+ id.hash
+ end
+
+ # Freeze the attributes hash such that associations are still accessible, even on destroyed records.
+ def freeze
+ @attributes.freeze; self
+ end
+
+ # Returns +true+ if the attributes hash has been frozen.
+ def frozen?
+ @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
+ nil
+ 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?
+ @readonly
+ end
+
+ # Marks this record as read only.
+ def readonly!
+ @readonly = true
+ end
+
+ # Returns the connection currently associated with the class. This can
+ # also be used to "borrow" the connection to do database work that isn't
+ # easily done without going straight to SQL.
+ def connection
+ self.class.connection
+ end
+
+ # Returns the contents of the record as a nicely formatted string.
+ def inspect
+ inspection = if @attributes
+ self.class.column_names.collect { |name|
+ if has_attribute?(name)
+ "#{name}: #{attribute_for_inspect(name)}"
+ end
+ }.compact.join(", ")
+ else
+ "not initialized"
+ end
+ "#<#{self.class} #{inspection}>"
+ end
+
+ # Hackery to accomodate Syck. Remove for 4.0.
+ def to_yaml(opts = {}) #:nodoc:
+ if YAML.const_defined?(:ENGINE) && !YAML::ENGINE.syck?
+ super
+ else
+ coder = {}
+ encode_with(coder)
+ YAML.quick_emit(self, opts) do |out|
+ out.map(taguri, to_yaml_style) do |map|
+ coder.each { |k, v| map.add(k, v) }
+ end
+ end
+ end
+ end
+
+ # Hackery to accomodate Syck. Remove for 4.0.
+ def yaml_initialize(tag, coder) #:nodoc:
+ init_with(coder)
+ end
+
+ private
+
+ # Under Ruby 1.9, Array#flatten will call #to_ary (recursively) on each of the elements
+ # of the array, and then rescues from the possible NoMethodError. If those elements are
+ # ActiveRecord::Base's, then this triggers the various method_missing's that we have,
+ # which significantly impacts upon performance.
+ #
+ # So we can avoid the method_missing hit by explicitly defining #to_ary as nil here.
+ #
+ # See also http://tenderlovemaking.com/2011/06/28/til-its-ok-to-return-nil-from-to_ary/
+ def to_ary # :nodoc:
+ nil
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb
index 3c7defedac..031918712a 100644
--- a/activerecord/lib/active_record/counter_cache.rb
+++ b/activerecord/lib/active_record/counter_cache.rb
@@ -65,7 +65,7 @@ module ActiveRecord
# Post.update_counters [10, 15], :comment_count => 1
# # Executes the following SQL:
# # UPDATE posts
- # # SET comment_count = COALESCE(comment_count, 0) + 1,
+ # # SET comment_count = COALESCE(comment_count, 0) + 1
# # WHERE id IN (10, 15)
def update_counters(id, counters)
updates = counters.map do |counter_name, value|
diff --git a/activerecord/lib/active_record/dynamic_matchers.rb b/activerecord/lib/active_record/dynamic_matchers.rb
new file mode 100644
index 0000000000..e9068089f0
--- /dev/null
+++ b/activerecord/lib/active_record/dynamic_matchers.rb
@@ -0,0 +1,79 @@
+module ActiveRecord
+ module DynamicMatchers
+ def respond_to?(method_id, include_private = false)
+ if match = DynamicFinderMatch.match(method_id)
+ return true if all_attributes_exists?(match.attribute_names)
+ elsif match = DynamicScopeMatch.match(method_id)
+ return true if all_attributes_exists?(match.attribute_names)
+ end
+
+ super
+ end
+
+ private
+
+ # Enables dynamic finders like <tt>User.find_by_user_name(user_name)</tt> and
+ # <tt>User.scoped_by_user_name(user_name). Refer to Dynamic attribute-based finders
+ # section at the top of this file for more detailed information.
+ #
+ # It's even possible to use all the additional parameters to +find+. For example, the
+ # full interface for +find_all_by_amount+ is actually <tt>find_all_by_amount(amount, options)</tt>.
+ #
+ # Each dynamic finder using <tt>scoped_by_*</tt> is also defined in the class after it
+ # is first invoked, so that future attempts to use it do not run through method_missing.
+ def method_missing(method_id, *arguments, &block)
+ if match = (DynamicFinderMatch.match(method_id) || DynamicScopeMatch.match(method_id))
+ attribute_names = match.attribute_names
+ super unless all_attributes_exists?(attribute_names)
+ if arguments.size < attribute_names.size
+ method_trace = "#{__FILE__}:#{__LINE__}:in `#{method_id}'"
+ backtrace = [method_trace] + caller
+ raise ArgumentError, "wrong number of arguments (#{arguments.size} for #{attribute_names.size})", backtrace
+ end
+ if match.respond_to?(:scope?) && match.scope?
+ self.class_eval <<-METHOD, __FILE__, __LINE__ + 1
+ def self.#{method_id}(*args) # def self.scoped_by_user_name_and_password(*args)
+ attributes = Hash[[:#{attribute_names.join(',:')}].zip(args)] # attributes = Hash[[:user_name, :password].zip(args)]
+ #
+ scoped(:conditions => attributes) # scoped(:conditions => attributes)
+ end # end
+ METHOD
+ send(method_id, *arguments)
+ elsif match.finder?
+ options = arguments.extract_options!
+ relation = options.any? ? scoped(options) : scoped
+ relation.send :find_by_attributes, match, attribute_names, *arguments, &block
+ elsif match.instantiator?
+ scoped.send :find_or_instantiator_by_attributes, match, attribute_names, *arguments, &block
+ end
+ else
+ super
+ end
+ end
+
+ # Similar in purpose to +expand_hash_conditions_for_aggregates+.
+ def expand_attribute_names_for_aggregates(attribute_names)
+ attribute_names.map { |attribute_name|
+ unless (aggregation = reflect_on_aggregation(attribute_name.to_sym)).nil?
+ aggregate_mapping(aggregation).map do |field_attr, _|
+ field_attr.to_sym
+ end
+ else
+ attribute_name.to_sym
+ end
+ }.flatten
+ end
+
+ def all_attributes_exists?(attribute_names)
+ (expand_attribute_names_for_aggregates(attribute_names) -
+ column_methods_hash.keys).empty?
+ end
+
+ def aggregate_mapping(reflection)
+ mapping = reflection.options[:mapping] || [reflection.name, reflection.name]
+ mapping.first.is_a?(Array) ? mapping : [mapping]
+ end
+
+
+ end
+end
diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb
index 8262b60f6e..fc80f3081e 100644
--- a/activerecord/lib/active_record/errors.rb
+++ b/activerecord/lib/active_record/errors.rb
@@ -99,6 +99,16 @@ module ActiveRecord
#
# Read more about optimistic locking in ActiveRecord::Locking module RDoc.
class StaleObjectError < ActiveRecordError
+ attr_reader :record, :attempted_action
+
+ def initialize(record, attempted_action)
+ @record = record
+ @attempted_action = attempted_action
+ end
+
+ def message
+ "Attempted to #{attempted_action} a stale object: #{record.class.name}"
+ end
end
# Raised when association is being configured improperly or
@@ -170,8 +180,7 @@ module ActiveRecord
end
end
- # Raised when a model attempts to fetch its primary key from the database, but the table
- # has no primary key declared.
+ # Raised when a primary key is needed, but there is not one specified in the schema or model.
class UnknownPrimaryKey < ActiveRecordError
attr_reader :model
diff --git a/activerecord/lib/active_record/explain.rb b/activerecord/lib/active_record/explain.rb
new file mode 100644
index 0000000000..e502d7e52b
--- /dev/null
+++ b/activerecord/lib/active_record/explain.rb
@@ -0,0 +1,80 @@
+require 'active_support/core_ext/class/attribute'
+
+module ActiveRecord
+ module Explain
+ def self.extended(base)
+ # If a query takes longer than these many seconds we log its query plan
+ # automatically. nil disables this feature.
+ base.config_attribute :auto_explain_threshold_in_seconds, :global => true
+ end
+
+ # If auto explain is enabled, this method triggers EXPLAIN logging for the
+ # queries triggered by the block if it takes more than the threshold as a
+ # whole. That is, the threshold is not checked against each individual
+ # query, but against the duration of the entire block. This approach is
+ # convenient for relations.
+ #
+ # The available_queries_for_explain thread variable collects the queries
+ # to be explained. If the value is nil, it means queries are not being
+ # currently collected. A false value indicates collecting is turned
+ # off. Otherwise it is an array of queries.
+ def logging_query_plan # :nodoc:
+ threshold = auto_explain_threshold_in_seconds
+ current = Thread.current
+ if threshold && current[:available_queries_for_explain].nil?
+ begin
+ queries = current[:available_queries_for_explain] = []
+ start = Time.now
+ result = yield
+ logger.warn(exec_explain(queries)) if Time.now - start > threshold
+ result
+ ensure
+ current[:available_queries_for_explain] = nil
+ end
+ else
+ yield
+ end
+ end
+
+ # Relation#explain needs to be able to collect the queries regardless of
+ # whether auto explain is enabled. This method serves that purpose.
+ def collecting_queries_for_explain # :nodoc:
+ current = Thread.current
+ original, current[:available_queries_for_explain] = current[:available_queries_for_explain], []
+ return yield, current[:available_queries_for_explain]
+ ensure
+ # Note that the return value above does not depend on this assigment.
+ current[:available_queries_for_explain] = original
+ end
+
+ # Makes the adapter execute EXPLAIN for the tuples of queries and bindings.
+ # Returns a formatted string ready to be logged.
+ def exec_explain(queries) # :nodoc:
+ queries && queries.map do |sql, bind|
+ [].tap do |msg|
+ msg << "EXPLAIN for: #{sql}"
+ unless bind.empty?
+ bind_msg = bind.map {|col, val| [col.name, val]}.inspect
+ msg.last << " #{bind_msg}"
+ end
+ msg << connection.explain(sql, bind)
+ end.join("\n")
+ end.join("\n")
+ end
+
+ # Silences automatic EXPLAIN logging for the duration of the block.
+ #
+ # This has high priority, no EXPLAINs will be run even if downwards
+ # the threshold is set to 0.
+ #
+ # As the name of the method suggests this only applies to automatic
+ # EXPLAINs, manual calls to +ActiveRecord::Relation#explain+ run.
+ def silence_auto_explain
+ current = Thread.current
+ original, current[:available_queries_for_explain] = current[:available_queries_for_explain], false
+ yield
+ ensure
+ current[:available_queries_for_explain] = original
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/explain_subscriber.rb b/activerecord/lib/active_record/explain_subscriber.rb
new file mode 100644
index 0000000000..fc76410499
--- /dev/null
+++ b/activerecord/lib/active_record/explain_subscriber.rb
@@ -0,0 +1,21 @@
+require 'active_support/notifications'
+
+module ActiveRecord
+ class ExplainSubscriber # :nodoc:
+ def call(*args)
+ if queries = Thread.current[:available_queries_for_explain]
+ payload = args.last
+ queries << payload.values_at(:sql, :binds) unless ignore_payload?(payload)
+ end
+ end
+
+ # SCHEMA queries cannot be EXPLAINed, also we do not want to run EXPLAIN on
+ # our own EXPLAINs now matter how loopingly beautiful that would be.
+ IGNORED_PAYLOADS = %w(SCHEMA EXPLAIN)
+ def ignore_payload?(payload)
+ payload[:exception] || IGNORED_PAYLOADS.include?(payload[:name])
+ end
+
+ ActiveSupport::Notifications.subscribe("sql.active_record", new)
+ end
+end
diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb
index 97af15c9e8..c59c00f424 100644
--- a/activerecord/lib/active_record/fixtures.rb
+++ b/activerecord/lib/active_record/fixtures.rb
@@ -10,7 +10,6 @@ require 'zlib'
require 'active_support/dependencies'
require 'active_support/core_ext/array/wrap'
require 'active_support/core_ext/object/blank'
-require 'active_support/core_ext/logger'
require 'active_support/ordered_hash'
require 'active_record/fixtures/file'
@@ -22,8 +21,6 @@ else
end
end
-class FixturesFileNotFound < StandardError; end
-
module ActiveRecord
# \Fixtures are a way of organizing data that you want to test against; in short, sample data.
#
@@ -391,10 +388,15 @@ module ActiveRecord
@@all_cached_fixtures = Hash.new { |h,k| h[k] = {} }
- def self.find_table_name(table_name) # :nodoc:
+ def self.default_fixture_model_name(fixture_name) # :nodoc:
ActiveRecord::Base.pluralize_table_names ?
- table_name.to_s.singularize.camelize :
- table_name.to_s.camelize
+ fixture_name.singularize.camelize :
+ fixture_name.camelize
+ end
+
+ def self.default_fixture_table_name(fixture_name) # :nodoc:
+ "#{ActiveRecord::Base.table_name_prefix}"\
+ "#{fixture_name.tr('/', '_')}#{ActiveRecord::Base.table_name_suffix}".to_sym
end
def self.reset_cache
@@ -444,9 +446,6 @@ module ActiveRecord
def self.create_fixtures(fixtures_directory, table_names, class_names = {})
table_names = [table_names].flatten.map { |n| n.to_s }
- table_names.each { |n|
- class_names[n.tr('/', '_').to_sym] = n.classify if n.include?('/')
- }
# FIXME: Apparently JK uses this.
connection = block_given? ? yield : ActiveRecord::Base.connection
@@ -460,12 +459,12 @@ module ActiveRecord
fixtures_map = {}
fixture_files = files_to_read.map do |path|
- table_name = path.tr '/', '_'
+ fixture_name = path
- fixtures_map[path] = ActiveRecord::Fixtures.new(
+ fixtures_map[fixture_name] = new( # ActiveRecord::Fixtures.new
connection,
- table_name,
- class_names[table_name.to_sym] || table_name.classify,
+ fixture_name,
+ class_names[fixture_name] || default_fixture_model_name(fixture_name),
::File.join(fixtures_directory, path))
end
@@ -489,8 +488,8 @@ module ActiveRecord
# Cap primary key sequences to max(pk).
if connection.respond_to?(:reset_pk_sequence!)
- table_names.each do |table_name|
- connection.reset_pk_sequence!(table_name.tr('/', '_'))
+ fixture_files.each do |ff|
+ connection.reset_pk_sequence!(ff.table_name)
end
end
end
@@ -509,25 +508,27 @@ module ActiveRecord
attr_reader :table_name, :name, :fixtures, :model_class
- def initialize(connection, table_name, class_name, fixture_path)
+ def initialize(connection, fixture_name, class_name, fixture_path)
@connection = connection
- @table_name = table_name
@fixture_path = fixture_path
- @name = table_name # preserve fixture base name
+ @name = fixture_name
@class_name = class_name
@fixtures = ActiveSupport::OrderedHash.new
- @table_name = "#{ActiveRecord::Base.table_name_prefix}#{@table_name}#{ActiveRecord::Base.table_name_suffix}"
# Should be an AR::Base type class
if class_name.is_a?(Class)
- @table_name = class_name.table_name
- @connection = class_name.connection
- @model_class = class_name
+ @model_class = class_name
else
- @model_class = class_name.constantize rescue nil
+ @model_class = class_name.constantize rescue nil
end
+ @connection = model_class.connection if model_class && model_class.respond_to?(:connection)
+
+ @table_name = ( model_class.respond_to?(:table_name) ?
+ model_class.table_name :
+ self.class.default_fixture_table_name(fixture_name) )
+
read_fixture_files
end
@@ -562,7 +563,7 @@ module ActiveRecord
rows[table_name] = fixtures.map do |label, fixture|
row = fixture.to_hash
- if model_class && model_class < ActiveRecord::Base
+ if model_class && model_class < ActiveRecord::Model
# fill in timestamp columns if they aren't specified and the model is set to record_timestamps
if model_class.record_timestamps
timestamp_column_names.each do |name|
@@ -622,7 +623,7 @@ module ActiveRecord
private
def primary_key_name
- @primary_key_name ||= model_class && model_class.primary_key? && model_class.primary_key
+ @primary_key_name ||= model_class && model_class.primary_key
end
def has_primary_key_column?
@@ -644,14 +645,6 @@ module ActiveRecord
end
def read_fixture_files
- if ::File.file?(yaml_file_path)
- read_yaml_fixture_files
- else
- raise FixturesFileNotFound, "Could not find #{yaml_file_path}"
- end
- end
-
- def read_yaml_fixture_files
yaml_files = Dir["#{@fixture_path}/**/*.yml"].select { |f|
::File.file?(f)
} + [yaml_file_path]
@@ -734,14 +727,29 @@ module ActiveRecord
self.use_instantiated_fixtures = false
self.pre_loaded_fixtures = false
- self.fixture_class_names = Hash.new do |h, table_name|
- h[table_name] = ActiveRecord::Fixtures.find_table_name(table_name)
+ self.fixture_class_names = Hash.new do |h, fixture_name|
+ h[fixture_name] = ActiveRecord::Fixtures.default_fixture_model_name(fixture_name)
end
end
module ClassMethods
+ # Sets the model class for a fixture when the class name cannot be inferred from the fixture name.
+ #
+ # Examples:
+ #
+ # set_fixture_class :some_fixture => SomeModel,
+ # 'namespaced/fixture' => Another::Model
+ #
+ # The keys must be the fixture names, that coincide with the short paths to the fixture files.
+ #--
+ # It is also possible to pass the class name instead of the class:
+ # set_fixture_class 'some_fixture' => 'SomeModel'
+ # I think this option is redundant, i propose to deprecate it.
+ # Isn't it easier to always pass the class itself?
+ # (2011-12-20 alexeymuranov)
+ #++
def set_fixture_class(class_names = {})
- self.fixture_class_names = self.fixture_class_names.merge(class_names)
+ self.fixture_class_names = self.fixture_class_names.merge(class_names.stringify_keys)
end
def fixtures(*fixture_names)
@@ -781,9 +789,9 @@ module ActiveRecord
fixture_names = Array.wrap(fixture_names || fixture_table_names)
methods = Module.new do
fixture_names.each do |fixture_name|
- fixture_name = fixture_name.to_s.tr('./', '_')
+ accessor_name = fixture_name.tr('/', '_').to_sym
- define_method(fixture_name) do |*fixtures|
+ define_method(accessor_name) do |*fixtures|
force_reload = fixtures.pop if fixtures.last == true || fixtures.last == :reload
@fixture_cache[fixture_name] ||= {}
@@ -796,13 +804,13 @@ module ActiveRecord
@fixture_cache[fixture_name][fixture] ||= @loaded_fixtures[fixture_name][fixture.to_s].find
end
else
- raise StandardError, "No fixture with name '#{fixture}' found for table '#{fixture_name}'"
+ raise StandardError, "No entry named '#{fixture}' found for fixture collection '#{fixture_name}'"
end
end
instances.size == 1 ? instances.first : instances
end
- private fixture_name
+ private accessor_name
end
end
include methods
@@ -832,6 +840,7 @@ module ActiveRecord
end
@fixture_cache = {}
+ @fixture_connections = []
@@already_loaded_fixtures ||= {}
# Load fixtures once and begin transaction.
@@ -842,9 +851,12 @@ module ActiveRecord
@loaded_fixtures = load_fixtures
@@already_loaded_fixtures[self.class] = @loaded_fixtures
end
- ActiveRecord::Base.connection.increment_open_transactions
- ActiveRecord::Base.connection.transaction_joinable = false
- ActiveRecord::Base.connection.begin_db_transaction
+ @fixture_connections = enlist_fixture_connections
+ @fixture_connections.each do |connection|
+ connection.increment_open_transactions
+ connection.transaction_joinable = false
+ connection.begin_db_transaction
+ end
# Load fixtures for every test.
else
ActiveRecord::Fixtures.reset_cache
@@ -864,13 +876,22 @@ module ActiveRecord
end
# Rollback changes if a transaction is active.
- if run_in_transaction? && ActiveRecord::Base.connection.open_transactions != 0
- ActiveRecord::Base.connection.rollback_db_transaction
- ActiveRecord::Base.connection.decrement_open_transactions
+ if run_in_transaction?
+ @fixture_connections.each do |connection|
+ if connection.open_transactions != 0
+ connection.rollback_db_transaction
+ connection.decrement_open_transactions
+ end
+ end
+ @fixture_connections.clear
end
ActiveRecord::Base.clear_active_connections!
end
+ def enlist_fixture_connections
+ ActiveRecord::Base.connection_handler.connection_pools.values.map(&:connection)
+ end
+
private
def load_fixtures
fixtures = ActiveRecord::Fixtures.create_fixtures(fixture_path, fixture_table_names, fixture_class_names)
diff --git a/activerecord/lib/active_record/identity_map.rb b/activerecord/lib/active_record/identity_map.rb
index b15b5a8133..680d9ffea0 100644
--- a/activerecord/lib/active_record/identity_map.rb
+++ b/activerecord/lib/active_record/identity_map.rb
@@ -111,13 +111,12 @@ module ActiveRecord
# model object.
def reinit_with(coder)
@attributes_cache = {}
- dirty = @changed_attributes.keys
- @attributes.update(coder['attributes'].except(*dirty))
+ dirty = @changed_attributes.keys
+ attributes = self.class.initialize_attributes(coder['attributes'].except(*dirty))
+ @attributes.update(attributes)
@changed_attributes.update(coder['attributes'].slice(*dirty))
@changed_attributes.delete_if{|k,v| v.eql? @attributes[k]}
- set_serialized_attributes
-
run_callbacks :find
self
diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb
new file mode 100644
index 0000000000..eaa7deac5a
--- /dev/null
+++ b/activerecord/lib/active_record/inheritance.rb
@@ -0,0 +1,182 @@
+require 'active_support/concern'
+
+module ActiveRecord
+ module Inheritance
+ extend ActiveSupport::Concern
+
+ included do
+ # Determine whether to store the full constant name including namespace when using STI
+ config_attribute :store_full_sti_class
+ self.store_full_sti_class = true
+ end
+
+ module ClassMethods
+ # True if this isn't a concrete subclass needing a STI type condition.
+ def descends_from_active_record?
+ sup = active_record_super
+
+ if sup.abstract_class?
+ sup.descends_from_active_record?
+ elsif self == Base
+ false
+ else
+ [Base, Model].include?(sup) || !columns_hash.include?(inheritance_column)
+ end
+ end
+
+ def finder_needs_type_condition? #:nodoc:
+ # This is like this because benchmarking justifies the strange :false stuff
+ :true == (@finder_needs_type_condition ||= descends_from_active_record? ? :false : :true)
+ end
+
+ def symbolized_base_class
+ @symbolized_base_class ||= base_class.to_s.to_sym
+ end
+
+ def symbolized_sti_name
+ @symbolized_sti_name ||= sti_name.present? ? sti_name.to_sym : symbolized_base_class
+ end
+
+ # Returns the base AR subclass that this class descends from. If A
+ # extends AR::Base, A.base_class will return A. If B descends from A
+ # through some arbitrarily deep hierarchy, B.base_class will return A.
+ #
+ # If B < A and C < B and if A is an abstract_class then both B.base_class
+ # and C.base_class would return B as the answer since A is an abstract_class.
+ def base_class
+ class_of_active_record_descendant(self)
+ end
+
+ # Set this to true if this is an abstract class (see <tt>abstract_class?</tt>).
+ attr_accessor :abstract_class
+
+ # Returns whether this class is an abstract class or not.
+ def abstract_class?
+ defined?(@abstract_class) && @abstract_class == true
+ end
+
+ def sti_name
+ store_full_sti_class ? name : name.demodulize
+ end
+
+ # Finder methods must instantiate through this method to work with the
+ # single-table inheritance model that makes it possible to create
+ # objects of different types from the same table.
+ def instantiate(record)
+ sti_class = find_sti_class(record[inheritance_column])
+ record_id = sti_class.primary_key && record[sti_class.primary_key]
+
+ if ActiveRecord::IdentityMap.enabled? && record_id
+ if (column = sti_class.columns_hash[sti_class.primary_key]) && column.number?
+ record_id = record_id.to_i
+ end
+ if instance = IdentityMap.get(sti_class, record_id)
+ instance.reinit_with('attributes' => record)
+ else
+ instance = sti_class.allocate.init_with('attributes' => record)
+ IdentityMap.add(instance)
+ end
+ else
+ instance = sti_class.allocate.init_with('attributes' => record)
+ end
+
+ instance
+ end
+
+ # For internal use.
+ #
+ # If this class includes ActiveRecord::Model then it won't have a
+ # superclass. So this provides a way to get to the 'root' (ActiveRecord::Model).
+ def active_record_super #:nodoc:
+ superclass < Model ? superclass : Model
+ end
+
+ protected
+
+ # Returns the class descending directly from ActiveRecord::Base or an
+ # abstract class, if any, in the inheritance hierarchy.
+ def class_of_active_record_descendant(klass)
+ unless klass < Model
+ raise ActiveRecordError, "#{name} doesn't belong in a hierarchy descending from ActiveRecord"
+ end
+
+ sup = klass.active_record_super
+ if [Base, Model].include?(klass) || [Base, Model].include?(sup) || sup.abstract_class?
+ klass
+ else
+ class_of_active_record_descendant(sup)
+ end
+ end
+
+ # Returns the class type of the record using the current module as a prefix. So descendants of
+ # MyApp::Business::Account would appear as MyApp::Business::AccountSubclass.
+ def compute_type(type_name)
+ if type_name.match(/^::/)
+ # If the type is prefixed with a scope operator then we assume that
+ # the type_name is an absolute reference.
+ ActiveSupport::Dependencies.constantize(type_name)
+ else
+ # Build a list of candidates to search for
+ candidates = []
+ name.scan(/::|$/) { candidates.unshift "#{$`}::#{type_name}" }
+ candidates << type_name
+
+ candidates.each do |candidate|
+ begin
+ constant = ActiveSupport::Dependencies.constantize(candidate)
+ return constant if candidate == constant.to_s
+ rescue NameError => e
+ # We don't want to swallow NoMethodError < NameError errors
+ raise e unless e.instance_of?(NameError)
+ end
+ end
+
+ raise NameError, "uninitialized constant #{candidates.first}"
+ end
+ end
+
+ private
+
+ def find_sti_class(type_name)
+ if type_name.blank? || !columns_hash.include?(inheritance_column)
+ self
+ else
+ begin
+ if store_full_sti_class
+ ActiveSupport::Dependencies.constantize(type_name)
+ else
+ compute_type(type_name)
+ end
+ rescue NameError
+ raise SubclassNotFound,
+ "The single-table inheritance mechanism failed to locate the subclass: '#{type_name}'. " +
+ "This error is raised because the column '#{inheritance_column}' is reserved for storing the class in case of inheritance. " +
+ "Please rename this column if you didn't intend it to be used for storing the inheritance class " +
+ "or overwrite #{name}.inheritance_column to use another column for that information."
+ end
+ end
+ end
+
+ def type_condition(table = arel_table)
+ sti_column = table[inheritance_column.to_sym]
+ sti_names = ([self] + descendants).map { |model| model.sti_name }
+
+ sti_column.in(sti_names)
+ end
+ end
+
+ private
+
+ # Sets the attribute used for single table inheritance to this class name if this is not the
+ # ActiveRecord::Base descendant.
+ # Considering the hierarchy Reply < Message < ActiveRecord::Base, this makes it possible to
+ # do Reply.new without having to set <tt>Reply[Reply.inheritance_column] = "Reply"</tt> yourself.
+ # No such attribute would be set for objects of the Message class in that example.
+ def ensure_proper_type
+ klass = self.class
+ if klass.finder_needs_type_condition?
+ write_attribute(klass.inheritance_column, klass.sti_name)
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb
new file mode 100644
index 0000000000..2c42f4cca5
--- /dev/null
+++ b/activerecord/lib/active_record/integration.rb
@@ -0,0 +1,49 @@
+module ActiveRecord
+ module Integration
+ # Returns a String, which Action Pack uses for constructing an URL to this
+ # object. The default implementation returns this record's id as a String,
+ # or nil if this record's unsaved.
+ #
+ # For example, suppose that you have a User model, and that you have a
+ # <tt>resources :users</tt> route. Normally, +user_path+ will
+ # construct a path with the user object's 'id' in it:
+ #
+ # user = User.find_by_name('Phusion')
+ # user_path(user) # => "/users/1"
+ #
+ # You can override +to_param+ in your model to make +user_path+ construct
+ # a path using the user's name instead of the user's id:
+ #
+ # class User < ActiveRecord::Base
+ # def to_param # overridden
+ # name
+ # end
+ # end
+ #
+ # user = User.find_by_name('Phusion')
+ # user_path(user) # => "/users/Phusion"
+ def to_param
+ # We can't use alias_method here, because method 'id' optimizes itself on the fly.
+ id && id.to_s # Be sure to stringify the id for routes
+ end
+
+ # Returns a cache key that can be used to identify this record.
+ #
+ # ==== Examples
+ #
+ # 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
+ case
+ when new_record?
+ "#{self.class.model_name.cache_key}/new"
+ when timestamp = self[:updated_at]
+ timestamp = timestamp.utc.to_s(:number)
+ "#{self.class.model_name.cache_key}/#{id}-#{timestamp}"
+ else
+ "#{self.class.model_name.cache_key}/#{id}"
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb
index d9ad7e4132..e643c0d437 100644
--- a/activerecord/lib/active_record/locking/optimistic.rb
+++ b/activerecord/lib/active_record/locking/optimistic.rb
@@ -37,6 +37,9 @@ module ActiveRecord
# You're then responsible for dealing with the conflict by rescuing the exception and either rolling back, merging,
# or otherwise apply the business logic needed to resolve the conflict.
#
+ # This locking mechanism will function inside a single Ruby process. To make it work across all
+ # web requests, the recommended approach is to add +lock_version+ as a hidden field to your form.
+ #
# You must ensure that your database schema defaults the +lock_version+ column to 0.
#
# This behavior can be turned off by setting <tt>ActiveRecord::Base.lock_optimistically = false</tt>.
@@ -46,12 +49,8 @@ module ActiveRecord
extend ActiveSupport::Concern
included do
- cattr_accessor :lock_optimistically, :instance_writer => false
+ config_attribute :lock_optimistically, :global => true
self.lock_optimistically = true
-
- class << self
- alias_method :locking_column=, :set_locking_column
- end
end
def locking_enabled? #:nodoc:
@@ -65,21 +64,6 @@ module ActiveRecord
send(lock_col + '=', previous_lock_value + 1)
end
- def attributes_from_column_definition
- result = super
-
- # If the locking column has no default value set,
- # start the lock version at zero. Note we can't use
- # <tt>locking_enabled?</tt> at this point as
- # <tt>@attributes</tt> may not have been initialized yet.
-
- if result.key?(self.class.locking_column) && lock_optimistically
- result[self.class.locking_column] ||= 0
- end
-
- result
- end
-
def update(attribute_names = @attributes.keys) #:nodoc:
return super unless locking_enabled?
return 0 if attribute_names.empty?
@@ -103,7 +87,7 @@ module ActiveRecord
affected_rows = connection.update stmt
unless affected_rows == 1
- raise ActiveRecord::StaleObjectError, "Attempted to update a stale object: #{self.class.name}"
+ raise ActiveRecord::StaleObjectError.new(self, "update")
end
affected_rows
@@ -127,7 +111,7 @@ module ActiveRecord
affected_rows = self.class.unscoped.where(predicate).delete_all
unless affected_rows == 1
- raise ActiveRecord::StaleObjectError, "Attempted to delete a stale object: #{self.class.name}"
+ raise ActiveRecord::StaleObjectError.new(self, "destroy")
end
end
@@ -146,14 +130,15 @@ module ActiveRecord
end
# Set the column to use for optimistic locking. Defaults to +lock_version+.
- def set_locking_column(value = nil, &block)
- define_attr_method :locking_column, value, &block
- value
+ def locking_column=(value)
+ @original_locking_column = @locking_column if defined?(@locking_column)
+ @locking_column = value.to_s
end
# The version column used for optimistic locking. Defaults to +lock_version+.
def locking_column
- reset_locking_column
+ reset_locking_column unless defined?(@locking_column)
+ @locking_column
end
# Quote the column name used for optimistic locking.
@@ -163,7 +148,7 @@ module ActiveRecord
# Reset the column used for optimistic locking back to the +lock_version+ default.
def reset_locking_column
- set_locking_column DEFAULT_LOCKING_COLUMN
+ self.locking_column = DEFAULT_LOCKING_COLUMN
end
# Make sure the lock version column gets updated when counters are
@@ -172,6 +157,18 @@ module ActiveRecord
counters = counters.merge(locking_column => 1) if locking_enabled?
super
end
+
+ # If the locking column has no default value set,
+ # start the lock version at zero. Note we can't use
+ # <tt>locking_enabled?</tt> at this point as
+ # <tt>@attributes</tt> may not have been initialized yet.
+ def initialize_attributes(attributes) #:nodoc:
+ if attributes.key?(locking_column) && lock_optimistically
+ attributes[locking_column] ||= 0
+ end
+
+ attributes
+ end
end
end
end
diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb
index 3a015ee8c2..a25f2c7bca 100644
--- a/activerecord/lib/active_record/log_subscriber.rb
+++ b/activerecord/lib/active_record/log_subscriber.rb
@@ -26,9 +26,9 @@ module ActiveRecord
return if 'SCHEMA' == payload[:name]
- name = '%s (%.1fms)' % [payload[:name], event.duration]
- sql = payload[:sql].squeeze(' ')
- binds = nil
+ name = '%s (%.1fms)' % [payload[:name], event.duration]
+ sql = payload[:sql].squeeze(' ')
+ binds = nil
unless (payload[:binds] || []).empty?
binds = " " + payload[:binds].map { |col,v|
diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb
index 7166f1b82a..46464783fd 100644
--- a/activerecord/lib/active_record/migration.rb
+++ b/activerecord/lib/active_record/migration.rb
@@ -112,12 +112,13 @@ 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_name)</tt>: Removes the column named
- # +column_name+ from the table called +table_name+.
+ # * <tt>remove_column(table_name, column_names)</tt>: Removes the column listed in
+ # +column_names+ 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> and <tt>:unique</tt> (e.g.
- # <tt>{ :name => "users_name_index", :unique => true }</tt>).
+ # <tt>:name</tt>, <tt>:unique</tt> (e.g.
+ # <tt>{ :name => "users_name_index", :unique => true }</tt>) and <tt>:order</tt>
+ # (e.g. { :order => {:name => :desc} }</tt>).
# * <tt>remove_index(table_name, :column => column_name)</tt>: Removes the index
# specified by +column_name+.
# * <tt>remove_index(table_name, :name => index_name)</tt>: Removes the index
@@ -301,7 +302,7 @@ module ActiveRecord
#
# class TenderloveMigration < ActiveRecord::Migration
# def change
- # create_table(:horses) do
+ # create_table(:horses) do |t|
# t.column :content, :text
# t.column :remind_at, :datetime
# end
@@ -442,6 +443,7 @@ module ActiveRecord
say_with_time "#{method}(#{arg_list})" do
unless arguments.empty? || method == :execute
arguments[0] = Migrator.proper_table_name(arguments.first)
+ arguments[1] = Migrator.proper_table_name(arguments.second) if method == :rename_table
end
return super unless connection.respond_to?(method)
connection.send(method, *arguments, &block)
@@ -455,26 +457,28 @@ module ActiveRecord
destination_migrations = ActiveRecord::Migrator.migrations(destination)
last = destination_migrations.last
- sources.each do |name, path|
+ sources.each do |scope, path|
source_migrations = ActiveRecord::Migrator.migrations(path)
source_migrations.each do |migration|
source = File.read(migration.filename)
- source = "# This migration comes from #{name} (originally #{migration.version})\n#{source}"
+ source = "# This migration comes from #{scope} (originally #{migration.version})\n#{source}"
if duplicate = destination_migrations.detect { |m| m.name == migration.name }
- options[:on_skip].call(name, migration) if File.read(duplicate.filename) != source && options[:on_skip]
+ if options[:on_skip] && duplicate.scope != scope.to_s
+ options[:on_skip].call(scope, migration)
+ end
next
end
migration.version = next_migration_number(last ? last.version + 1 : 0).to_i
- new_path = File.join(destination, "#{migration.version}_#{migration.name.underscore}.rb")
+ new_path = File.join(destination, "#{migration.version}_#{migration.name.underscore}.#{scope}.rb")
old_path, migration.filename = migration.filename, new_path
last = migration
- FileUtils.cp(old_path, migration.filename)
+ File.open(migration.filename, "w") { |f| f.write source }
copied << migration
- options[:on_copy].call(name, migration, old_path) if options[:on_copy]
+ options[:on_copy].call(scope, migration, old_path) if options[:on_copy]
destination_migrations << migration
end
end
@@ -493,9 +497,9 @@ module ActiveRecord
# MigrationProxy is used to defer loading of the actual migration classes
# until they are needed
- class MigrationProxy < Struct.new(:name, :version, :filename)
+ class MigrationProxy < Struct.new(:name, :version, :filename, :scope)
- def initialize(name, version, filename)
+ def initialize(name, version, filename, scope)
super
@migration = nil
end
@@ -524,16 +528,16 @@ module ActiveRecord
attr_writer :migrations_paths
alias :migrations_path= :migrations_paths=
- def migrate(migrations_paths, target_version = nil)
+ def migrate(migrations_paths, target_version = nil, &block)
case
when target_version.nil?
- up(migrations_paths, target_version)
+ up(migrations_paths, target_version, &block)
when current_version == 0 && target_version == 0
[]
when current_version > target_version
- down(migrations_paths, target_version)
+ down(migrations_paths, target_version, &block)
else
- up(migrations_paths, target_version)
+ up(migrations_paths, target_version, &block)
end
end
@@ -545,12 +549,12 @@ module ActiveRecord
move(:up, migrations_paths, steps)
end
- def up(migrations_paths, target_version = nil)
- self.new(:up, migrations_paths, target_version).migrate
+ def up(migrations_paths, target_version = nil, &block)
+ self.new(:up, migrations_paths, target_version).migrate(&block)
end
- def down(migrations_paths, target_version = nil)
- self.new(:down, migrations_paths, target_version).migrate
+ def down(migrations_paths, target_version = nil, &block)
+ self.new(:down, migrations_paths, target_version).migrate(&block)
end
def run(direction, migrations_paths, target_version)
@@ -590,15 +594,16 @@ module ActiveRecord
migrations_paths.first
end
- def migrations(paths)
+ def migrations(paths, subdirectories = true)
paths = Array.wrap(paths)
- files = Dir[*paths.map { |p| "#{p}/[0-9]*_*.rb" }]
+ glob = subdirectories ? "**/" : ""
+ files = Dir[*paths.map { |p| "#{p}/#{glob}[0-9]*_*.rb" }]
seen = Hash.new false
migrations = files.map do |file|
- version, name = file.scan(/([0-9]+)_([_a-z0-9]*).rb/).first
+ version, name, scope = file.scan(/([0-9]+)_([_a-z0-9]*)\.?([_a-z0-9]*)?.rb/).first
raise IllegalMigrationNameError.new(file) unless version
version = version.to_i
@@ -609,7 +614,7 @@ module ActiveRecord
seen[version] = seen[name] = true
- MigrationProxy.new(name, version, file)
+ MigrationProxy.new(name, version, file, scope)
end
migrations.sort_by(&:version)
@@ -652,7 +657,7 @@ module ActiveRecord
end
end
- def migrate
+ def migrate(&block)
current = migrations.detect { |m| m.version == current_version }
target = migrations.detect { |m| m.version == @target_version }
@@ -669,6 +674,10 @@ module ActiveRecord
ran = []
runnable.each do |migration|
+ if block && !block.call(migration)
+ next
+ end
+
Base.logger.info "Migrating to #{migration.name} (#{migration.version})" if Base.logger
seen = migrated.include?(migration.version.to_i)
diff --git a/activerecord/lib/active_record/model.rb b/activerecord/lib/active_record/model.rb
new file mode 100644
index 0000000000..86de5ab2fa
--- /dev/null
+++ b/activerecord/lib/active_record/model.rb
@@ -0,0 +1,109 @@
+require 'active_support/deprecation'
+
+module ActiveRecord
+ # <tt>ActiveRecord::Model</tt> can be included into a class to add Active Record persistence.
+ # This is an alternative to inheriting from <tt>ActiveRecord::Base</tt>. Example:
+ #
+ # class Post
+ # include ActiveRecord::Model
+ # end
+ #
+ module Model
+ module ClassMethods #:nodoc:
+ include ActiveSupport::Callbacks::ClassMethods
+ include ActiveModel::Naming
+ include QueryCache::ClassMethods
+ include ActiveSupport::Benchmarkable
+ include ActiveSupport::DescendantsTracker
+
+ include Querying
+ include Translation
+ include DynamicMatchers
+ include CounterCache
+ include Explain
+ include ConnectionHandling
+ end
+
+ def self.included(base)
+ return if base.singleton_class < ClassMethods
+
+ base.class_eval do
+ extend ClassMethods
+ Callbacks::Register.setup(self)
+ initialize_generated_modules unless self == Base
+ end
+ end
+
+ extend ActiveModel::Configuration
+ extend ActiveModel::Callbacks
+ extend ActiveModel::MassAssignmentSecurity::ClassMethods
+ extend ActiveModel::AttributeMethods::ClassMethods
+ extend Callbacks::Register
+ extend Explain
+ extend ConnectionHandling
+
+ def self.extend(*modules)
+ ClassMethods.send(:include, *modules)
+ end
+
+ include Persistence
+ include ReadonlyAttributes
+ include ModelSchema
+ include Inheritance
+ include Scoping
+ include Sanitization
+ include Integration
+ include AttributeAssignment
+ include ActiveModel::Conversion
+ include Validations
+ include Locking::Optimistic, Locking::Pessimistic
+ include AttributeMethods
+ include Callbacks, ActiveModel::Observing, Timestamp
+ include Associations
+ include IdentityMap
+ include ActiveModel::SecurePassword
+ include AutosaveAssociation, NestedAttributes
+ include Aggregations, Transactions, Reflection, Serialization, Store
+ include Core
+
+ class << self
+ def arel_engine
+ self
+ end
+
+ def abstract_class?
+ false
+ end
+
+ def inheritance_column
+ 'type'
+ end
+ end
+
+ module DeprecationProxy #:nodoc:
+ class << self
+ instance_methods.each { |m| undef_method m unless m =~ /^__|^object_id$|^instance_eval$/ }
+
+ def method_missing(name, *args, &block)
+ if Model.respond_to?(name)
+ Model.send(name, *args, &block)
+ else
+ ActiveSupport::Deprecation.warn(
+ "The object passed to the active_record load hook was previously ActiveRecord::Base " \
+ "(a Class). Now it is ActiveRecord::Model (a Module). You have called `#{name}' which " \
+ "is only defined on ActiveRecord::Base. Please change your code so that it works with " \
+ "a module rather than a class. (Model is included in Base, so anything added to Model " \
+ "will be available on Base as well.)"
+ )
+ Base.send(name, *args, &block)
+ end
+ end
+
+ alias send method_missing
+ end
+ end
+ end
+
+ # Load Base at this point, because the active_record load hook is run in that file.
+ Base
+end
diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb
new file mode 100644
index 0000000000..c1a8583119
--- /dev/null
+++ b/activerecord/lib/active_record/model_schema.rb
@@ -0,0 +1,303 @@
+require 'active_support/concern'
+require 'active_support/core_ext/class/attribute_accessors'
+
+module ActiveRecord
+ module ModelSchema
+ extend ActiveSupport::Concern
+
+ included do
+ ##
+ # :singleton-method:
+ # Accessor for the prefix type that will be prepended to every primary key column name.
+ # The options are :table_name and :table_name_with_underscore. If the first is specified,
+ # the Product class will look for "productid" instead of "id" as the primary column. If the
+ # latter is specified, the Product class will look for "product_id" instead of "id". Remember
+ # that this is a global setting for all Active Records.
+ config_attribute :primary_key_prefix_type, :global => true
+
+ ##
+ # :singleton-method:
+ # Accessor for the name of the prefix string to prepend to every table name. So if set
+ # to "basecamp_", all table names will be named like "basecamp_projects", "basecamp_people",
+ # etc. This is a convenient way of creating a namespace for tables in a shared database.
+ # By default, the prefix is the empty string.
+ #
+ # If you are organising your models within modules you can add a prefix to the models within
+ # a namespace by defining a singleton method in the parent module called table_name_prefix which
+ # returns your chosen prefix.
+ config_attribute :table_name_prefix
+ self.table_name_prefix = ""
+
+ ##
+ # :singleton-method:
+ # Works like +table_name_prefix+, but appends instead of prepends (set to "_basecamp" gives "projects_basecamp",
+ # "people_basecamp"). By default, the suffix is the empty string.
+ config_attribute :table_name_suffix
+ self.table_name_suffix = ""
+
+ ##
+ # :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.
+ config_attribute :pluralize_table_names
+ self.pluralize_table_names = true
+ end
+
+ module ClassMethods
+ # Guesses the table name (in forced lower-case) based on the name of the class in the
+ # inheritance hierarchy descending directly from ActiveRecord::Base. So if the hierarchy
+ # looks like: Reply < Message < ActiveRecord::Base, then Message is used
+ # to guess the table name even when called on Reply. The rules used to do the guess
+ # are handled by the Inflector class in Active Support, which knows almost all common
+ # English inflections. You can add new inflections in config/initializers/inflections.rb.
+ #
+ # Nested classes are given table names prefixed by the singular form of
+ # the parent's table name. Enclosing modules are not considered.
+ #
+ # ==== Examples
+ #
+ # class Invoice < ActiveRecord::Base
+ # end
+ #
+ # file class table_name
+ # invoice.rb Invoice invoices
+ #
+ # class Invoice < ActiveRecord::Base
+ # class Lineitem < ActiveRecord::Base
+ # end
+ # end
+ #
+ # file class table_name
+ # invoice.rb Invoice::Lineitem invoice_lineitems
+ #
+ # module Invoice
+ # class Lineitem < ActiveRecord::Base
+ # end
+ # end
+ #
+ # file class table_name
+ # invoice/lineitem.rb Invoice::Lineitem lineitems
+ #
+ # Additionally, the class-level +table_name_prefix+ is prepended and the
+ # +table_name_suffix+ is appended. So if you have "myapp_" as a prefix,
+ # the table name guess for an Invoice class becomes "myapp_invoices".
+ # Invoice::Lineitem becomes "myapp_invoice_lineitems".
+ #
+ # You can also set your own table name explicitly:
+ #
+ # class Mouse < ActiveRecord::Base
+ # self.table_name = "mice"
+ # end
+ #
+ # Alternatively, you can override the table_name method to define your
+ # own computation. (Possibly using <tt>super</tt> to manipulate the default
+ # table name.) Example:
+ #
+ # class Post < ActiveRecord::Base
+ # def self.table_name
+ # "special_" + super
+ # end
+ # end
+ # Post.table_name # => "special_posts"
+ def table_name
+ reset_table_name unless defined?(@table_name)
+ @table_name
+ end
+
+ # Sets the table name explicitly. Example:
+ #
+ # class Project < ActiveRecord::Base
+ # self.table_name = "project"
+ # end
+ #
+ # You can also just define your own <tt>self.table_name</tt> method; see
+ # the documentation for ActiveRecord::Base#table_name.
+ def table_name=(value)
+ @original_table_name = @table_name if defined?(@table_name)
+ @table_name = value
+ @quoted_table_name = nil
+ @arel_table = nil
+ @relation = Relation.new(self, arel_table)
+ end
+
+ # Returns a quoted version of the table name, used to construct SQL statements.
+ def quoted_table_name
+ @quoted_table_name ||= connection.quote_table_name(table_name)
+ end
+
+ # Computes the table name, (re)sets it internally, and returns it.
+ def reset_table_name #:nodoc:
+ if active_record_super.abstract_class?
+ self.table_name = active_record_super.table_name || compute_table_name
+ elsif abstract_class?
+ self.table_name = active_record_super == Base ? nil : active_record_super.table_name
+ else
+ self.table_name = compute_table_name
+ end
+ end
+
+ def full_table_name_prefix #:nodoc:
+ (parents.detect{ |p| p.respond_to?(:table_name_prefix) } || self).table_name_prefix
+ end
+
+ # The name of the column containing the object's class when Single Table Inheritance is used
+ def inheritance_column
+ (@inheritance_column ||= nil) || active_record_super.inheritance_column
+ end
+
+ # Sets the value of inheritance_column
+ def inheritance_column=(value)
+ @original_inheritance_column = inheritance_column
+ @inheritance_column = value.to_s
+ end
+
+ def sequence_name
+ if base_class == self
+ @sequence_name ||= reset_sequence_name
+ else
+ (@sequence_name ||= nil) || base_class.sequence_name
+ end
+ end
+
+ def reset_sequence_name #:nodoc:
+ self.sequence_name = connection.default_sequence_name(table_name, primary_key)
+ end
+
+ # Sets the name of the sequence to use when generating ids to the given
+ # value, or (if the value is nil or false) to the value returned by the
+ # given block. This is required for Oracle and is useful for any
+ # database which relies on sequences for primary key generation.
+ #
+ # If a sequence name is not explicitly set when using Oracle or Firebird,
+ # it will default to the commonly used pattern of: #{table_name}_seq
+ #
+ # If a sequence name is not explicitly set when using PostgreSQL, it
+ # will discover the sequence corresponding to your primary key for you.
+ #
+ # class Project < ActiveRecord::Base
+ # self.sequence_name = "projectseq" # default would have been "project_seq"
+ # end
+ def sequence_name=(value)
+ @original_sequence_name = @sequence_name if defined?(@sequence_name)
+ @sequence_name = value.to_s
+ end
+
+ # Indicates whether the table associated with this class exists
+ def table_exists?
+ connection.schema_cache.table_exists?(table_name)
+ end
+
+ # Returns an array of column objects for the table associated with this class.
+ def columns
+ @columns ||= connection.schema_cache.columns[table_name].map do |col|
+ col = col.dup
+ col.primary = (col.name == primary_key)
+ col
+ end
+ end
+
+ # Returns a hash of column objects for the table associated with this class.
+ def columns_hash
+ @columns_hash ||= Hash[columns.map { |c| [c.name, c] }]
+ end
+
+ # Returns a hash where the keys are column names and the values are
+ # default values when instantiating the AR object for this table.
+ def column_defaults
+ @column_defaults ||= Hash[columns.map { |c| [c.name, c.default] }]
+ end
+
+ # Returns an array of column names as strings.
+ def column_names
+ @column_names ||= columns.map { |column| column.name }
+ end
+
+ # Returns an array of column objects where the primary id, all columns ending in "_id" or "_count",
+ # and columns used for single table inheritance have been removed.
+ def content_columns
+ @content_columns ||= columns.reject { |c| c.primary || c.name =~ /(_id|_count)$/ || c.name == inheritance_column }
+ end
+
+ # Returns a hash of all the methods added to query each of the columns in the table with the name of the method as the key
+ # and true as the value. This makes it possible to do O(1) lookups in respond_to? to check if a given method for attribute
+ # is available.
+ def column_methods_hash #:nodoc:
+ @dynamic_methods_hash ||= column_names.inject(Hash.new(false)) do |methods, attr|
+ attr_name = attr.to_s
+ methods[attr.to_sym] = attr_name
+ methods["#{attr}=".to_sym] = attr_name
+ methods["#{attr}?".to_sym] = attr_name
+ methods["#{attr}_before_type_cast".to_sym] = attr_name
+ methods
+ end
+ end
+
+ # Resets all the cached information about columns, which will cause them
+ # to be reloaded on the next request.
+ #
+ # The most common usage pattern for this method is probably in a migration,
+ # when just after creating a table you want to populate it with some default
+ # values, eg:
+ #
+ # class CreateJobLevels < ActiveRecord::Migration
+ # def up
+ # create_table :job_levels do |t|
+ # t.integer :id
+ # t.string :name
+ #
+ # t.timestamps
+ # end
+ #
+ # JobLevel.reset_column_information
+ # %w{assistant executive manager director}.each do |type|
+ # JobLevel.create(:name => type)
+ # end
+ # end
+ #
+ # def down
+ # drop_table :job_levels
+ # end
+ # end
+ def reset_column_information
+ connection.clear_cache!
+ undefine_attribute_methods
+ connection.schema_cache.clear_table_cache!(table_name) if table_exists?
+
+ @column_names = @content_columns = @column_defaults = @columns = @columns_hash = nil
+ @dynamic_methods_hash = @inheritance_column = nil
+ @arel_engine = @relation = nil
+ end
+
+ def clear_cache! # :nodoc:
+ connection.schema_cache.clear!
+ end
+
+ private
+
+ # Guesses the table name, but does not decorate it with prefix and suffix information.
+ def undecorated_table_name(class_name = base_class.name)
+ table_name = class_name.to_s.demodulize.underscore
+ table_name = table_name.pluralize if pluralize_table_names
+ table_name
+ end
+
+ # Computes and returns a table name according to default conventions.
+ def compute_table_name
+ base = base_class
+ if self == base
+ # Nested classes are prefixed with singular parent table name.
+ if parent < ActiveRecord::Model && !parent.abstract_class?
+ contained = parent.table_name
+ contained = contained.singularize if parent.pluralize_table_names
+ contained += '_'
+ end
+ "#{full_table_name_prefix}#{contained}#{undecorated_table_name(name)}#{table_name_suffix}"
+ else
+ # STI subclasses always use their superclass' table.
+ base.table_name
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/named_scope.rb b/activerecord/lib/active_record/named_scope.rb
deleted file mode 100644
index 0313abe456..0000000000
--- a/activerecord/lib/active_record/named_scope.rb
+++ /dev/null
@@ -1,200 +0,0 @@
-require 'active_support/core_ext/array'
-require 'active_support/core_ext/hash/except'
-require 'active_support/core_ext/kernel/singleton_class'
-require 'active_support/core_ext/object/blank'
-require 'active_support/core_ext/class/attribute'
-
-module ActiveRecord
- # = Active Record Named \Scopes
- module NamedScope
- extend ActiveSupport::Concern
-
- module ClassMethods
- # Returns an anonymous \scope.
- #
- # posts = Post.scoped
- # posts.size # Fires "select count(*) from posts" and returns the count
- # posts.each {|p| puts p.name } # Fires "select * from posts" and loads post objects
- #
- # fruits = Fruit.scoped
- # fruits = fruits.where(:color => 'red') if options[:red_only]
- # fruits = fruits.limit(10) if limited?
- #
- # Anonymous \scopes tend to be useful when procedurally generating complex
- # queries, where passing intermediate values (\scopes) around as first-class
- # objects is convenient.
- #
- # You can define a \scope that applies to all finders using
- # ActiveRecord::Base.default_scope.
- def scoped(options = nil)
- if options
- scoped.apply_finder_options(options)
- else
- if current_scope
- current_scope.clone
- else
- scope = relation.clone
- scope.default_scoped = true
- scope
- end
- end
- end
-
- ##
- # Collects attributes from scopes that should be applied when creating
- # an AR instance for the particular class this is called on.
- def scope_attributes # :nodoc:
- if current_scope
- current_scope.scope_for_create
- else
- scope = relation.clone
- scope.default_scoped = true
- scope.scope_for_create
- end
- end
-
- ##
- # Are there default attributes associated with this scope?
- def scope_attributes? # :nodoc:
- current_scope || default_scopes.any?
- end
-
- # Adds a class method for retrieving and querying objects. A \scope represents a narrowing of a database query,
- # such as <tt>where(:color => :red).select('shirts.*').includes(:washing_instructions)</tt>.
- #
- # class Shirt < ActiveRecord::Base
- # scope :red, where(:color => 'red')
- # scope :dry_clean_only, joins(:washing_instructions).where('washing_instructions.dry_clean_only = ?', true)
- # end
- #
- # The above calls to <tt>scope</tt> define class methods Shirt.red and Shirt.dry_clean_only. Shirt.red,
- # in effect, represents the query <tt>Shirt.where(:color => 'red')</tt>.
- #
- # Note that this is simply 'syntactic sugar' for defining an actual class method:
- #
- # class Shirt < ActiveRecord::Base
- # def self.red
- # where(:color => 'red')
- # end
- # end
- #
- # Unlike <tt>Shirt.find(...)</tt>, however, the object returned by Shirt.red is not an Array; it
- # resembles the association object constructed by a <tt>has_many</tt> declaration. For instance,
- # you can invoke <tt>Shirt.red.first</tt>, <tt>Shirt.red.count</tt>, <tt>Shirt.red.where(:size => 'small')</tt>.
- # Also, just as with the association objects, named \scopes act like an Array, implementing Enumerable;
- # <tt>Shirt.red.each(&block)</tt>, <tt>Shirt.red.first</tt>, and <tt>Shirt.red.inject(memo, &block)</tt>
- # all behave as if Shirt.red really was an Array.
- #
- # These named \scopes are composable. For instance, <tt>Shirt.red.dry_clean_only</tt> will produce
- # all shirts that are both red and dry clean only.
- # Nested finds and calculations also work with these compositions: <tt>Shirt.red.dry_clean_only.count</tt>
- # returns the number of garments for which these criteria obtain. Similarly with
- # <tt>Shirt.red.dry_clean_only.average(:thread_count)</tt>.
- #
- # All \scopes are available as class methods on the ActiveRecord::Base descendant upon which
- # the \scopes were defined. But they are also available to <tt>has_many</tt> associations. If,
- #
- # class Person < ActiveRecord::Base
- # has_many :shirts
- # end
- #
- # then <tt>elton.shirts.red.dry_clean_only</tt> will return all of Elton's red, dry clean
- # only shirts.
- #
- # Named \scopes can also be procedural:
- #
- # class Shirt < ActiveRecord::Base
- # scope :colored, lambda { |color| where(:color => color) }
- # end
- #
- # In this example, <tt>Shirt.colored('puce')</tt> finds all puce shirts.
- #
- # On Ruby 1.9 you can use the 'stabby lambda' syntax:
- #
- # scope :colored, ->(color) { where(:color => color) }
- #
- # Note that scopes defined with \scope will be evaluated when they are defined, rather than
- # when they are used. For example, the following would be incorrect:
- #
- # class Post < ActiveRecord::Base
- # scope :recent, where('published_at >= ?', Time.now - 1.week)
- # end
- #
- # The example above would be 'frozen' to the <tt>Time.now</tt> value when the <tt>Post</tt>
- # class was defined, and so the resultant SQL query would always be the same. The correct
- # way to do this would be via a lambda, which will re-evaluate the scope each time
- # it is called:
- #
- # class Post < ActiveRecord::Base
- # scope :recent, lambda { where('published_at >= ?', Time.now - 1.week) }
- # end
- #
- # Named \scopes can also have extensions, just as with <tt>has_many</tt> declarations:
- #
- # class Shirt < ActiveRecord::Base
- # scope :red, where(:color => 'red') do
- # def dom_id
- # 'red_shirts'
- # end
- # end
- # end
- #
- # Scopes can also be used while creating/building a record.
- #
- # class Article < ActiveRecord::Base
- # scope :published, where(:published => true)
- # end
- #
- # Article.published.new.published # => true
- # Article.published.create.published # => true
- #
- # Class methods on your model are automatically available
- # on scopes. Assuming the following setup:
- #
- # class Article < ActiveRecord::Base
- # scope :published, where(:published => true)
- # scope :featured, where(:featured => true)
- #
- # def self.latest_article
- # order('published_at desc').first
- # end
- #
- # def self.titles
- # map(&:title)
- # end
- #
- # end
- #
- # We are able to call the methods like this:
- #
- # Article.published.featured.latest_article
- # Article.featured.titles
-
- def scope(name, scope_options = {})
- name = name.to_sym
- valid_scope_name?(name)
- extension = Module.new(&Proc.new) if block_given?
-
- scope_proc = lambda do |*args|
- options = scope_options.respond_to?(:call) ? scope_options.call(*args) : scope_options
- options = scoped.apply_finder_options(options) if options.is_a?(Hash)
-
- relation = scoped.merge(options)
-
- extension ? relation.extending(extension) : relation
- end
-
- singleton_class.send(:redefine_method, name, &scope_proc)
- end
-
- protected
-
- def valid_scope_name?(name)
- if respond_to?(name, true)
- logger.warn "Creating scope :#{name}. " \
- "Overwriting existing method #{self.name}.#{name}."
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb
index 2dbebfcaf8..a8ee43b598 100644
--- a/activerecord/lib/active_record/nested_attributes.rb
+++ b/activerecord/lib/active_record/nested_attributes.rb
@@ -12,7 +12,7 @@ module ActiveRecord
extend ActiveSupport::Concern
included do
- class_attribute :nested_attributes_options, :instance_writer => false
+ config_attribute :nested_attributes_options
self.nested_attributes_options = {}
end
@@ -220,7 +220,7 @@ module ActiveRecord
# validates_presence_of :member
# end
module ClassMethods
- REJECT_ALL_BLANK_PROC = proc { |attributes| attributes.all? { |_, value| value.blank? } }
+ REJECT_ALL_BLANK_PROC = proc { |attributes| attributes.all? { |key, value| key == '_destroy' || value.blank? } }
# Defines an attributes writer for the specified association(s). If you
# are using <tt>attr_protected</tt> or <tt>attr_accessible</tt>, then you
@@ -239,7 +239,8 @@ module ActiveRecord
# is specified, a record will be built for all attribute hashes that
# do not have a <tt>_destroy</tt> value that evaluates to true.
# Passing <tt>:all_blank</tt> instead of a Proc will create a proc
- # that will reject a record where all the attributes are blank.
+ # that will reject a record where all the attributes are blank excluding
+ # any value for _destroy.
# [:limit]
# Allows you to specify the maximum number of the associated records that
# can be processed with the nested attributes. If the size of the
diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb
index b5dadb929d..09ee2ba61d 100644
--- a/activerecord/lib/active_record/persistence.rb
+++ b/activerecord/lib/active_record/persistence.rb
@@ -1,6 +1,53 @@
+require 'active_support/concern'
+
module ActiveRecord
# = Active Record Persistence
module Persistence
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ # Creates an object (or multiple objects) and saves it to the database, if validations pass.
+ # The resulting object is returned whether the object was saved successfully to the database or not.
+ #
+ # The +attributes+ parameter can be either a Hash or an Array of Hashes. These Hashes describe the
+ # attributes on the objects that are to be created.
+ #
+ # +create+ respects mass-assignment security and accepts either +:as+ or +:without_protection+ options
+ # in the +options+ parameter.
+ #
+ # ==== Examples
+ # # Create a single new object
+ # User.create(:first_name => 'Jamie')
+ #
+ # # Create a single new object using the :admin mass-assignment security role
+ # User.create({ :first_name => 'Jamie', :is_admin => true }, :as => :admin)
+ #
+ # # Create a single new object bypassing mass-assignment security
+ # User.create({ :first_name => 'Jamie', :is_admin => true }, :without_protection => true)
+ #
+ # # Create an Array of new objects
+ # User.create([{ :first_name => 'Jamie' }, { :first_name => 'Jeremy' }])
+ #
+ # # Create a single object and pass it into a block to set other attributes.
+ # User.create(:first_name => 'Jamie') do |u|
+ # u.is_admin = false
+ # end
+ #
+ # # Creating an Array of new objects using a block, where the block is executed for each object:
+ # User.create([{ :first_name => 'Jamie' }, { :first_name => 'Jeremy' }]) do |u|
+ # u.is_admin = false
+ # end
+ def create(attributes = nil, options = {}, &block)
+ if attributes.is_a?(Array)
+ attributes.collect { |attr| create(attr, options, &block) }
+ else
+ object = new(attributes, options, &block)
+ object.save
+ object
+ end
+ end
+ end
+
# Returns true if this object hasn't been saved yet -- that is, a record
# for the object doesn't exist in the data store yet; otherwise, returns false.
def new_record?
@@ -12,8 +59,8 @@ module ActiveRecord
@destroyed
end
- # Returns if the record is persisted, i.e. it's not a new record and it was
- # not destroyed.
+ # Returns true if the record is persisted, i.e. it's not a new record and it was
+ # not destroyed, otherwise returns false.
def persisted?
!(new_record? || destroyed?)
end
@@ -114,6 +161,7 @@ module ActiveRecord
became.instance_variable_set("@attributes_cache", @attributes_cache)
became.instance_variable_set("@new_record", new_record?)
became.instance_variable_set("@destroyed", destroyed?)
+ became.instance_variable_set("@errors", errors)
became.type = klass.name unless self.class.descends_from_active_record?
became
end
@@ -161,7 +209,7 @@ module ActiveRecord
# The following transaction covers any possible database side-effects of the
# attributes assignment. For example, setting the IDs of a child collection.
with_transaction_returning_status do
- self.assign_attributes(attributes, options)
+ assign_attributes(attributes, options)
save
end
end
@@ -172,7 +220,7 @@ module ActiveRecord
# The following transaction covers any possible database side-effects of the
# attributes assignment. For example, setting the IDs of a child collection.
with_transaction_returning_status do
- self.assign_attributes(attributes, options)
+ assign_attributes(attributes, options)
save!
end
end
@@ -237,7 +285,7 @@ module ActiveRecord
clear_association_cache
IdentityMap.without do
- fresh_object = self.class.unscoped { self.class.find(self.id, options) }
+ fresh_object = self.class.unscoped { self.class.find(id, options) }
@attributes.update(fresh_object.instance_variable_get('@attributes'))
end
@@ -314,19 +362,11 @@ module ActiveRecord
new_id = self.class.unscoped.insert attributes_values
- self.id ||= new_id if self.class.primary_key?
+ self.id ||= new_id if self.class.primary_key
IdentityMap.add(self) if IdentityMap.enabled?
@new_record = false
id
end
-
- # Initializes the attributes array with keys matching the columns from the linked table and
- # the values matching the corresponding default value of that column, so
- # that a new instance, or one populated from a passed-in Hash, still has all the attributes
- # that instances loaded from the database would.
- def attributes_from_column_definition
- self.class.column_defaults.dup
- end
end
end
diff --git a/activerecord/lib/active_record/query_cache.rb b/activerecord/lib/active_record/query_cache.rb
index 10c0dc6f2a..466d148901 100644
--- a/activerecord/lib/active_record/query_cache.rb
+++ b/activerecord/lib/active_record/query_cache.rb
@@ -28,9 +28,10 @@ module ActiveRecord
end
class BodyProxy # :nodoc:
- def initialize(original_cache_value, target)
+ def initialize(original_cache_value, target, connection_id)
@original_cache_value = original_cache_value
@target = target
+ @connection_id = connection_id
end
def method_missing(method_sym, *arguments, &block)
@@ -48,6 +49,7 @@ module ActiveRecord
def close
@target.close if @target.respond_to?(:close)
ensure
+ ActiveRecord::Base.connection_id = @connection_id
ActiveRecord::Base.connection.clear_query_cache
unless @original_cache_value
ActiveRecord::Base.connection.disable_query_cache!
@@ -60,7 +62,7 @@ module ActiveRecord
ActiveRecord::Base.connection.enable_query_cache!
status, headers, body = @app.call(env)
- [status, headers, BodyProxy.new(old, body)]
+ [status, headers, BodyProxy.new(old, body, ActiveRecord::Base.connection_id)]
rescue Exception => e
ActiveRecord::Base.connection.clear_query_cache
unless old
diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb
new file mode 100644
index 0000000000..09da9ad1d1
--- /dev/null
+++ b/activerecord/lib/active_record/querying.rb
@@ -0,0 +1,58 @@
+require 'active_support/core_ext/module/delegation'
+
+module ActiveRecord
+ module Querying
+ delegate :find, :first, :first!, :last, :last!, :all, :exists?, :any?, :many?, :to => :scoped
+ delegate :first_or_create, :first_or_create!, :first_or_initialize, :to => :scoped
+ delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, :to => :scoped
+ delegate :find_each, :find_in_batches, :to => :scoped
+ delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins,
+ :where, :preload, :eager_load, :includes, :from, :lock, :readonly,
+ :having, :create_with, :uniq, :to => :scoped
+ delegate :count, :average, :minimum, :maximum, :sum, :calculate, :pluck, :to => :scoped
+
+ # Executes a custom SQL query against your database and returns all the results. The results will
+ # be returned as an array with columns requested encapsulated as attributes of the model you call
+ # this method from. If you call <tt>Product.find_by_sql</tt> then the results will be returned in
+ # a Product object with the attributes you specified in the SQL query.
+ #
+ # If you call a complicated SQL query which spans multiple tables the columns specified by the
+ # SELECT will be attributes of the model, whether or not they are columns of the corresponding
+ # table.
+ #
+ # The +sql+ parameter is a full SQL query as a string. It will be called as is, there will be
+ # no database agnostic conversions performed. This should be a last resort because using, for example,
+ # MySQL specific terms will lock you to using that particular database engine or require you to
+ # change your call if you switch engines.
+ #
+ # ==== Examples
+ # # A simple SQL query spanning multiple tables
+ # Post.find_by_sql "SELECT p.title, c.author FROM posts p, comments c WHERE p.id = c.post_id"
+ # > [#<Post:0x36bff9c @attributes={"title"=>"Ruby Meetup", "first_name"=>"Quentin"}>, ...]
+ #
+ # # You can use the same string replacement techniques as you can with ActiveRecord#find
+ # Post.find_by_sql ["SELECT title FROM posts WHERE author = ? AND created > ?", author_id, start_date]
+ # > [#<Post:0x36bff9c @attributes={"title"=>"The Cheap Man Buys Twice"}>, ...]
+ def find_by_sql(sql, binds = [])
+ logging_query_plan do
+ connection.select_all(sanitize_sql(sql), "#{name} Load", binds).collect! { |record| instantiate(record) }
+ end
+ end
+
+ # Returns the result of an SQL statement that should only include a COUNT(*) in the SELECT part.
+ # The use of this method should be restricted to complicated SQL queries that can't be executed
+ # using the ActiveRecord::Calculations class methods. Look into those before using this.
+ #
+ # ==== Parameters
+ #
+ # * +sql+ - An SQL statement which should return a count query from the database, see the example below.
+ #
+ # ==== Examples
+ #
+ # Product.count_by_sql "SELECT COUNT(*) FROM sales s, customers c WHERE s.customer_id = c.id"
+ def count_by_sql(sql)
+ sql = sanitize_conditions(sql)
+ connection.select_value(sql, "#{name} Count").to_i
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb
index 47133e77e8..165d0750dd 100644
--- a/activerecord/lib/active_record/railtie.rb
+++ b/activerecord/lib/active_record/railtie.rb
@@ -22,6 +22,13 @@ module ActiveRecord
config.app_middleware.insert_after "::ActionDispatch::Callbacks",
"ActiveRecord::ConnectionAdapters::ConnectionManagement"
+ config.action_dispatch.rescue_responses.merge!(
+ 'ActiveRecord::RecordNotFound' => :not_found,
+ 'ActiveRecord::StaleObjectError' => :conflict,
+ 'ActiveRecord::RecordInvalid' => :unprocessable_entity,
+ 'ActiveRecord::RecordNotSaved' => :unprocessable_entity
+ )
+
rake_tasks do
load "active_record/railties/databases.rake"
end
@@ -78,18 +85,30 @@ module ActiveRecord
end
end
- initializer "active_record.set_dispatch_hooks", :before => :set_clear_dependencies_hook do |app|
- ActiveSupport.on_load(:active_record) do
- ActionDispatch::Reloader.to_cleanup do
- ActiveRecord::Base.clear_reloadable_connections!
- ActiveRecord::Base.clear_cache!
+ initializer "active_record.set_reloader_hooks" do |app|
+ hook = lambda do
+ ActiveRecord::Base.clear_reloadable_connections!
+ ActiveRecord::Base.clear_cache!
+ end
+
+ if app.config.reload_classes_only_on_change
+ ActiveSupport.on_load(:active_record) do
+ ActionDispatch::Reloader.to_prepare(&hook)
+ end
+ else
+ ActiveSupport.on_load(:active_record) do
+ ActionDispatch::Reloader.to_cleanup(&hook)
end
end
end
+ initializer "active_record.add_watchable_files" do |app|
+ config.watchable_files.concat ["#{app.root}/db/schema.rb", "#{app.root}/db/structure.sql"]
+ end
+
config.after_initialize do
ActiveSupport.on_load(:active_record) do
- instantiate_observers
+ ActiveRecord::Base.instantiate_observers
ActionDispatch::Reloader.to_prepare do
ActiveRecord::Base.instantiate_observers
diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake
index f4a813d704..180424641a 100644
--- a/activerecord/lib/active_record/railties/databases.rake
+++ b/activerecord/lib/active_record/railties/databases.rake
@@ -1,8 +1,8 @@
require 'active_support/core_ext/object/inclusion'
+require 'active_record'
db_namespace = namespace :db do
task :load_config => :rails_env do
- require 'active_record'
ActiveRecord::Base.configurations = Rails.application.config.database_configuration
ActiveRecord::Migrator.migrations_paths = Rails.application.paths['db/migrate'].to_a
@@ -37,11 +37,8 @@ db_namespace = namespace :db do
desc 'Create the database from config/database.yml for the current Rails.env (use db:create:all to create all dbs in the config)'
task :create => :load_config do
- # Make the test database at the same time as the development one, if it exists
- if Rails.env.development? && ActiveRecord::Base.configurations['test']
- create_database(ActiveRecord::Base.configurations['test'])
- end
- create_database(ActiveRecord::Base.configurations[Rails.env])
+ configs_for_environment.each { |config| create_database(config) }
+ ActiveRecord::Base.establish_connection(configs_for_environment.first)
end
def mysql_creation_options(config)
@@ -115,8 +112,7 @@ db_namespace = namespace :db do
end
end
else
- # Bug with 1.9.2 Calling return within begin still executes else
- $stderr.puts "#{config['database']} already exists" unless config['adapter'] =~ /sqlite/
+ $stderr.puts "#{config['database']} already exists"
end
end
@@ -138,12 +134,7 @@ db_namespace = namespace :db do
desc 'Drops the database for the current Rails.env (use db:drop:all to drop all databases)'
task :drop => :load_config do
- config = ActiveRecord::Base.configurations[Rails.env || 'development']
- begin
- drop_database(config)
- rescue Exception => e
- $stderr.puts "Couldn't drop #{config['database']} : #{e.inspect}"
- end
+ configs_for_environment.each { |config| drop_database_and_rescue(config) }
end
def local_database?(config, &block)
@@ -158,8 +149,19 @@ db_namespace = namespace :db do
desc "Migrate the database (options: VERSION=x, VERBOSE=false)."
task :migrate => [:environment, :load_config] do
ActiveRecord::Migration.verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] == "true" : true
- ActiveRecord::Migrator.migrate(ActiveRecord::Migrator.migrations_paths, ENV["VERSION"] ? ENV["VERSION"].to_i : nil)
- db_namespace["schema:dump"].invoke if ActiveRecord::Base.schema_format == :ruby
+ ActiveRecord::Migrator.migrate(ActiveRecord::Migrator.migrations_paths, ENV["VERSION"] ? ENV["VERSION"].to_i : nil) do |migration|
+ ENV["SCOPE"].blank? || (ENV["SCOPE"] == migration.scope)
+ end
+ db_namespace['_dump'].invoke
+ end
+
+ task :_dump do
+ case ActiveRecord::Base.schema_format
+ when :ruby then db_namespace["schema:dump"].invoke
+ when :sql then db_namespace["structure:dump"].invoke
+ else
+ raise "unknown schema format #{ActiveRecord::Base.schema_format}"
+ end
end
namespace :migrate do
@@ -182,7 +184,7 @@ db_namespace = namespace :db do
version = ENV['VERSION'] ? ENV['VERSION'].to_i : nil
raise 'VERSION is required' unless version
ActiveRecord::Migrator.run(:up, ActiveRecord::Migrator.migrations_paths, version)
- db_namespace['schema:dump'].invoke if ActiveRecord::Base.schema_format == :ruby
+ db_namespace['_dump'].invoke
end
# desc 'Runs the "down" for a given migration VERSION.'
@@ -190,7 +192,7 @@ db_namespace = namespace :db do
version = ENV['VERSION'] ? ENV['VERSION'].to_i : nil
raise 'VERSION is required' unless version
ActiveRecord::Migrator.run(:down, ActiveRecord::Migrator.migrations_paths, version)
- db_namespace['schema:dump'].invoke if ActiveRecord::Base.schema_format == :ruby
+ db_namespace['_dump'].invoke
end
desc 'Display status of migrations'
@@ -230,18 +232,21 @@ db_namespace = namespace :db do
task :rollback => [:environment, :load_config] do
step = ENV['STEP'] ? ENV['STEP'].to_i : 1
ActiveRecord::Migrator.rollback(ActiveRecord::Migrator.migrations_paths, step)
- db_namespace['schema:dump'].invoke if ActiveRecord::Base.schema_format == :ruby
+ db_namespace['_dump'].invoke
end
# desc 'Pushes the schema to the next version (specify steps w/ STEP=n).'
task :forward => [:environment, :load_config] do
step = ENV['STEP'] ? ENV['STEP'].to_i : 1
ActiveRecord::Migrator.forward(ActiveRecord::Migrator.migrations_paths, step)
- db_namespace['schema:dump'].invoke if ActiveRecord::Base.schema_format == :ruby
+ db_namespace['_dump'].invoke
end
# desc 'Drops and recreates the database from db/schema.rb for the current environment and loads the seeds.'
- task :reset => [ 'db:drop', 'db:setup' ]
+ task :reset => :environment do
+ db_namespace["drop"].invoke
+ db_namespace["setup"].invoke
+ end
# desc "Retrieves the charset for the current environment's database"
task :charset => :environment do
@@ -280,24 +285,23 @@ db_namespace = namespace :db do
# desc "Raises an error if there are pending migrations"
task :abort_if_pending_migrations => :environment do
- if defined? ActiveRecord
- pending_migrations = ActiveRecord::Migrator.new(:up, ActiveRecord::Migrator.migrations_paths).pending_migrations
+ pending_migrations = ActiveRecord::Migrator.new(:up, ActiveRecord::Migrator.migrations_paths).pending_migrations
- if pending_migrations.any?
- puts "You have #{pending_migrations.size} pending migrations:"
- pending_migrations.each do |pending_migration|
- puts ' %4d %s' % [pending_migration.version, pending_migration.name]
- end
- abort %{Run "rake db:migrate" to update your database then try again.}
+ if pending_migrations.any?
+ puts "You have #{pending_migrations.size} pending migrations:"
+ pending_migrations.each do |pending_migration|
+ puts ' %4d %s' % [pending_migration.version, pending_migration.name]
end
+ abort %{Run `rake db:migrate` to update your database then try again.}
end
end
desc 'Create the database, load the schema, and initialize with the seed data (use db:reset to also drop the db first)'
- task :setup => [ 'db:create', 'db:schema:load', 'db:seed' ]
+ task :setup => ['db:schema:load_if_ruby', 'db:structure:load_if_sql', :seed]
desc 'Load the seed data from db/seeds.rb'
- task :seed => 'db:abort_if_pending_migrations' do
+ task :seed do
+ db_namespace['abort_if_pending_migrations'].invoke
Rails.application.load_seed
end
@@ -357,93 +361,125 @@ db_namespace = namespace :db do
if File.exists?(file)
load(file)
else
- abort %{#{file} doesn't exist yet. Run "rake db:migrate" to create it then try again. 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}
+ abort %{#{file} doesn't exist yet. Run `rake db:migrate` to create it then try again. 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}
end
end
+
+ task :load_if_ruby => 'db:create' do
+ db_namespace["schema:load"].invoke if ActiveRecord::Base.schema_format == :ruby
+ end
end
namespace :structure do
- desc 'Dump the database structure to an SQL file'
+ desc 'Dump the database structure to db/structure.sql. Specify another file with DB_STRUCTURE=db/my_structure.sql'
task :dump => :environment do
abcs = ActiveRecord::Base.configurations
+ filename = ENV['DB_STRUCTURE'] || File.join(Rails.root, "db", "structure.sql")
case abcs[Rails.env]['adapter']
when /mysql/, 'oci', 'oracle'
ActiveRecord::Base.establish_connection(abcs[Rails.env])
- File.open("#{Rails.root}/db/#{Rails.env}_structure.sql", "w+") { |f| f << ActiveRecord::Base.connection.structure_dump }
+ File.open(filename, "w:utf-8") { |f| f << ActiveRecord::Base.connection.structure_dump }
when /postgresql/
- ENV['PGHOST'] = abcs[Rails.env]['host'] if abcs[Rails.env]['host']
- ENV['PGPORT'] = abcs[Rails.env]["port"].to_s if abcs[Rails.env]['port']
- ENV['PGPASSWORD'] = abcs[Rails.env]['password'].to_s if abcs[Rails.env]['password']
+ set_psql_env(abcs[Rails.env])
search_path = abcs[Rails.env]['schema_search_path']
unless search_path.blank?
search_path = search_path.split(",").map{|search_path_part| "--schema=#{search_path_part.strip}" }.join(" ")
end
- `pg_dump -i -U "#{abcs[Rails.env]['username']}" -s -x -O -f db/#{Rails.env}_structure.sql #{search_path} #{abcs[Rails.env]['database']}`
+ `pg_dump -i -s -x -O -f #{filename} #{search_path} #{abcs[Rails.env]['database']}`
raise 'Error dumping database' if $?.exitstatus == 1
when /sqlite/
- dbfile = abcs[Rails.env]['database'] || abcs[Rails.env]['dbfile']
- `sqlite3 #{dbfile} .schema > db/#{Rails.env}_structure.sql`
+ dbfile = abcs[Rails.env]['database']
+ `sqlite3 #{dbfile} .schema > #{filename}`
when 'sqlserver'
- `smoscript -s #{abcs[Rails.env]['host']} -d #{abcs[Rails.env]['database']} -u #{abcs[Rails.env]['username']} -p #{abcs[Rails.env]['password']} -f db\\#{Rails.env}_structure.sql -A -U`
+ `smoscript -s #{abcs[Rails.env]['host']} -d #{abcs[Rails.env]['database']} -u #{abcs[Rails.env]['username']} -p #{abcs[Rails.env]['password']} -f #{filename} -A -U`
when "firebird"
set_firebird_env(abcs[Rails.env])
db_string = firebird_db_string(abcs[Rails.env])
- sh "isql -a #{db_string} > #{Rails.root}/db/#{Rails.env}_structure.sql"
+ sh "isql -a #{db_string} > #{filename}"
else
raise "Task not supported by '#{abcs[Rails.env]["adapter"]}'"
end
if ActiveRecord::Base.connection.supports_migrations?
- File.open("#{Rails.root}/db/#{Rails.env}_structure.sql", "a") { |f| f << ActiveRecord::Base.connection.dump_schema_information }
+ File.open(filename, "a") { |f| f << ActiveRecord::Base.connection.dump_schema_information }
end
end
- end
-
- namespace :test do
- # desc "Recreate the test database from the current schema.rb"
- task :load => 'db:test:purge' do
- ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['test'])
- ActiveRecord::Schema.verbose = false
- db_namespace['schema:load'].invoke
- end
- # desc "Recreate the test database from the current environment's database schema"
- task :clone => %w(db:schema:dump db:test:load)
+ # desc "Recreate the databases from the structure.sql file"
+ task :load => [:environment, :load_config] do
+ env = ENV['RAILS_ENV'] || 'test'
- # desc "Recreate the test databases from the development structure"
- task :clone_structure => [ 'db:structure:dump', 'db:test:purge' ] do
abcs = ActiveRecord::Base.configurations
- case abcs['test']['adapter']
+ filename = ENV['DB_STRUCTURE'] || File.join(Rails.root, "db", "structure.sql")
+ case abcs[env]['adapter']
when /mysql/
- ActiveRecord::Base.establish_connection(:test)
+ ActiveRecord::Base.establish_connection(abcs[env])
ActiveRecord::Base.connection.execute('SET foreign_key_checks = 0')
- IO.readlines("#{Rails.root}/db/#{Rails.env}_structure.sql").join.split("\n\n").each do |table|
+ IO.read(filename).split("\n\n").each do |table|
ActiveRecord::Base.connection.execute(table)
end
when /postgresql/
- ENV['PGHOST'] = abcs['test']['host'] if abcs['test']['host']
- ENV['PGPORT'] = abcs['test']['port'].to_s if abcs['test']['port']
- ENV['PGPASSWORD'] = abcs['test']['password'].to_s if abcs['test']['password']
- `psql -U "#{abcs['test']['username']}" -f #{Rails.root}/db/#{Rails.env}_structure.sql #{abcs['test']['database']} #{abcs['test']['template']}`
+ set_psql_env(abcs[env])
+ `psql -f "#{filename}" #{abcs[env]['database']} #{abcs[env]['template']}`
when /sqlite/
- dbfile = abcs['test']['database'] || abcs['test']['dbfile']
- `sqlite3 #{dbfile} < #{Rails.root}/db/#{Rails.env}_structure.sql`
+ dbfile = abcs[env]['database']
+ `sqlite3 #{dbfile} < "#{filename}"`
when 'sqlserver'
- `sqlcmd -S #{abcs['test']['host']} -d #{abcs['test']['database']} -U #{abcs['test']['username']} -P #{abcs['test']['password']} -i db\\#{Rails.env}_structure.sql`
+ `sqlcmd -S #{abcs[env]['host']} -d #{abcs[env]['database']} -U #{abcs[env]['username']} -P #{abcs[env]['password']} -i #{filename}`
when 'oci', 'oracle'
- ActiveRecord::Base.establish_connection(:test)
- IO.readlines("#{Rails.root}/db/#{Rails.env}_structure.sql").join.split(";\n\n").each do |ddl|
+ ActiveRecord::Base.establish_connection(abcs[env])
+ IO.read(filename).split(";\n\n").each do |ddl|
ActiveRecord::Base.connection.execute(ddl)
end
when 'firebird'
- set_firebird_env(abcs['test'])
- db_string = firebird_db_string(abcs['test'])
- sh "isql -i #{Rails.root}/db/#{Rails.env}_structure.sql #{db_string}"
+ set_firebird_env(abcs[env])
+ db_string = firebird_db_string(abcs[env])
+ sh "isql -i #{filename} #{db_string}"
else
- raise "Task not supported by '#{abcs['test']['adapter']}'"
+ raise "Task not supported by '#{abcs[env]['adapter']}'"
end
end
+ task :load_if_sql => 'db:create' do
+ db_namespace["structure:load"].invoke if ActiveRecord::Base.schema_format == :sql
+ end
+ end
+
+ namespace :test do
+
+ # desc "Recreate the test database from the current schema"
+ task :load => 'db:test:purge' do
+ case ActiveRecord::Base.schema_format
+ when :ruby
+ db_namespace["test:load_schema"].invoke
+ when :sql
+ db_namespace["test:load_structure"].invoke
+ end
+ end
+
+ # desc "Recreate the test database from an existent structure.sql file"
+ task :load_structure => 'db:test:purge' do
+ begin
+ old_env, ENV['RAILS_ENV'] = ENV['RAILS_ENV'], 'test'
+ db_namespace["structure:load"].invoke
+ ensure
+ ENV['RAILS_ENV'] = old_env
+ end
+ end
+
+ # desc "Recreate the test database from an existent schema.rb file"
+ task :load_schema => 'db:test:purge' do
+ ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['test'])
+ ActiveRecord::Schema.verbose = false
+ db_namespace["schema:load"].invoke
+ end
+
+ # desc "Recreate the test database from a fresh schema.rb file"
+ task :clone => %w(db:schema:dump db:test:load_schema)
+
+ # desc "Recreate the test database from a fresh structure.sql file"
+ task :clone_structure => [ "db:structure:dump", "db:test:load_structure" ]
+
# desc "Empty the test database"
task :purge => :environment do
abcs = ActiveRecord::Base.configurations
@@ -456,7 +492,7 @@ db_namespace = namespace :db do
drop_database(abcs['test'])
create_database(abcs['test'])
when /sqlite/
- dbfile = abcs['test']['database'] || abcs['test']['dbfile']
+ dbfile = abcs['test']['database']
File.delete(dbfile) if File.exist?(dbfile)
when 'sqlserver'
test = abcs.deep_dup['test']
@@ -479,7 +515,7 @@ db_namespace = namespace :db do
# desc 'Check for pending migrations and load the test schema'
task :prepare => 'db:abort_if_pending_migrations' do
- if defined?(ActiveRecord) && !ActiveRecord::Base.configurations.blank?
+ unless ActiveRecord::Base.configurations.blank?
db_namespace[{ :sql => 'test:clone_structure', :ruby => 'test:load' }[ActiveRecord::Base.schema_format]].invoke
end
end
@@ -548,6 +584,20 @@ def drop_database(config)
end
end
+def drop_database_and_rescue(config)
+ begin
+ drop_database(config)
+ rescue Exception => e
+ $stderr.puts "Couldn't drop #{config['database']} : #{e.inspect}"
+ end
+end
+
+def configs_for_environment
+ environments = [Rails.env]
+ environments << 'test' if Rails.env.development?
+ ActiveRecord::Base.configurations.values_at(*environments).compact.reject { |config| config['database'].blank? }
+end
+
def session_table_name
ActiveRecord::SessionStore::Session.table_name
end
@@ -560,3 +610,10 @@ end
def firebird_db_string(config)
FireRuby::Database.db_string_for(config.symbolize_keys)
end
+
+def set_psql_env(config)
+ ENV['PGHOST'] = config['host'] if config['host']
+ ENV['PGPORT'] = config['port'].to_s if config['port']
+ ENV['PGPASSWORD'] = config['password'].to_s if config['password']
+ ENV['PGUSER'] = config['username'].to_s if config['username']
+end
diff --git a/activerecord/lib/active_record/readonly_attributes.rb b/activerecord/lib/active_record/readonly_attributes.rb
new file mode 100644
index 0000000000..836b15e2ce
--- /dev/null
+++ b/activerecord/lib/active_record/readonly_attributes.rb
@@ -0,0 +1,26 @@
+require 'active_support/concern'
+require 'active_support/core_ext/class/attribute'
+
+module ActiveRecord
+ module ReadonlyAttributes
+ extend ActiveSupport::Concern
+
+ included do
+ config_attribute :_attr_readonly
+ self._attr_readonly = []
+ end
+
+ module ClassMethods
+ # Attributes listed as readonly will be used to create a new record but update operations will
+ # ignore these fields.
+ def attr_readonly(*attributes)
+ self._attr_readonly = Set.new(attributes.map { |a| a.to_s }) + (self._attr_readonly || [])
+ end
+
+ # Returns an array of all the attributes that have been specified as readonly.
+ def readonly_attributes
+ self._attr_readonly
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb
index 120ff0cac6..60b73e9fe5 100644
--- a/activerecord/lib/active_record/reflection.rb
+++ b/activerecord/lib/active_record/reflection.rb
@@ -7,7 +7,8 @@ module ActiveRecord
extend ActiveSupport::Concern
included do
- class_attribute :reflections
+ extend ActiveModel::Configuration
+ config_attribute :reflections
self.reflections = {}
end
@@ -124,7 +125,7 @@ module ActiveRecord
# <tt>composed_of :balance, :class_name => 'Money'</tt> returns <tt>'Money'</tt>
# <tt>has_many :clients</tt> returns <tt>'Client'</tt>
def class_name
- @class_name ||= options[:class_name] || derive_class_name
+ @class_name ||= (options[:class_name] || derive_class_name).to_s
end
# Returns +true+ if +self+ and +other_aggregation+ have the same +name+ attribute, +active_record+ attribute,
@@ -213,11 +214,11 @@ module ActiveRecord
# klass option is necessary to support loading polymorphic associations
def association_primary_key(klass = nil)
- options[:primary_key] || (klass || self.klass).primary_key
+ options[:primary_key] || primary_key(klass || self.klass)
end
def active_record_primary_key
- @active_record_primary_key ||= options[:primary_key] || active_record.primary_key
+ @active_record_primary_key ||= options[:primary_key] || primary_key(active_record)
end
def counter_cache_column
@@ -262,6 +263,10 @@ module ActiveRecord
[self]
end
+ def nested?
+ false
+ end
+
# An array of arrays of conditions. Each item in the outside array corresponds to a reflection
# in the #chain. The inside arrays are simply conditions (and each condition may itself be
# a hash, array, arel predicate, etc...)
@@ -357,6 +362,10 @@ module ActiveRecord
active_record.name.foreign_key
end
end
+
+ def primary_key(klass)
+ klass.primary_key || raise(UnknownPrimaryKey.new(klass))
+ end
end
# Holds all the meta-data about a :through association as it was specified
@@ -429,7 +438,7 @@ module ActiveRecord
# of relevant reflections, plus any :source_type or polymorphic :as constraints.
def conditions
@conditions ||= begin
- conditions = source_reflection.conditions
+ conditions = source_reflection.conditions.map { |c| c.dup }
# Add to it the conditions from this reflection if necessary.
conditions.first << options[:conditions] if options[:conditions]
@@ -453,7 +462,7 @@ module ActiveRecord
source_reflection.source_macro
end
- # A through association is nested iff there would be more than one join table
+ # A through association is nested if there would be more than one join table
def nested?
chain.length > 2 || through_reflection.macro == :has_and_belongs_to_many
end
@@ -461,7 +470,7 @@ module ActiveRecord
# We want to use the klass from this reflection, rather than just delegate straight to
# the source_reflection, because the source_reflection may be polymorphic. We still
# need to respect the source_reflection's :primary_key option, though.
- def association_primary_key(klass = self.klass)
+ def association_primary_key(klass = nil)
# Get the "actual" source reflection if the immediate source reflection has a
# source reflection itself
source_reflection = self.source_reflection
@@ -469,7 +478,7 @@ module ActiveRecord
source_reflection = source_reflection.source_reflection
end
- source_reflection.options[:primary_key] || klass.primary_key
+ source_reflection.options[:primary_key] || primary_key(klass || self.klass)
end
# Gets an array of possible <tt>:through</tt> source reflection names:
diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb
index bf61d79a2c..6f2248fa21 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
@@ -1,5 +1,6 @@
+# -*- coding: utf-8 -*-
require 'active_support/core_ext/object/blank'
-require 'active_support/core_ext/module/delegation'
+require 'active_support/deprecation'
module ActiveRecord
# = Active Record Relation
@@ -7,13 +8,9 @@ module ActiveRecord
JoinOperation = Struct.new(:relation, :join_class, :on)
ASSOCIATION_METHODS = [:includes, :eager_load, :preload]
MULTI_VALUE_METHODS = [:select, :group, :order, :joins, :where, :having, :bind]
- SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :from, :reorder, :reverse_order]
+ SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :from, :reorder, :reverse_order, :uniq]
- include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches
-
- # These are explicitly delegated to improve performance (avoids method_missing)
- delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to => :to_a
- delegate :table_name, :quoted_table_name, :primary_key, :primary_key?, :quoted_primary_key, :connection, :column_hash,:to => :klass
+ include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches, Explain, Delegation
attr_reader :table, :klass, :loaded
attr_accessor :extensions, :default_scoped
@@ -36,7 +33,7 @@ module ActiveRecord
def insert(values)
primary_key_value = nil
- if primary_key? && Hash === values
+ if primary_key && Hash === values
primary_key_value = values[values.keys.find { |k|
k.name == primary_key
}]
@@ -70,7 +67,7 @@ module ActiveRecord
conn.insert(
im,
'SQL',
- primary_key? && primary_key,
+ primary_key,
primary_key_value,
nil,
binds)
@@ -136,14 +133,35 @@ module ActiveRecord
first || new(attributes, options, &block)
end
- def respond_to?(method, include_private = false)
- arel.respond_to?(method, include_private) ||
- Array.method_defined?(method) ||
- @klass.respond_to?(method, include_private) ||
- super
+ # Runs EXPLAIN on the query or queries triggered by this relation and
+ # returns the result as a string. The string is formatted imitating the
+ # ones printed by the database shell.
+ #
+ # Note that this method actually runs the queries, since the results of some
+ # are needed by the next ones when eager loading is going on.
+ #
+ # Please see further details in the
+ # {Active Record Query Interface guide}[http://edgeguides.rubyonrails.org/active_record_querying.html#running-explain].
+ def explain
+ _, queries = collecting_queries_for_explain { exec_queries }
+ exec_explain(queries)
end
def to_a
+ # We monitor here the entire execution rather than individual SELECTs
+ # because from the point of view of the user fetching the records of a
+ # relation is a single unit of work. You want to know if this call takes
+ # too long, not if the individual queries take too long.
+ #
+ # It could be the case that none of the queries involved surpass the
+ # threshold, and at the same time the sum of them all does. The user
+ # should get a query plan logged in that case.
+ logging_query_plan do
+ exec_queries
+ end
+ end
+
+ def exec_queries
return @records if loaded?
default_scoped = with_default_scope
@@ -174,6 +192,7 @@ module ActiveRecord
@loaded = true
@records
end
+ private :exec_queries
def as_json(options = nil) #:nodoc:
to_a.as_json(options)
@@ -219,7 +238,7 @@ module ActiveRecord
# Please check unscoped if you want to remove all previous scopes (including
# the default_scope) during the execution of a block.
def scoping
- @klass.send(:with_scope, self, :overwrite) { yield }
+ @klass.with_scope(self, :overwrite) { yield }
end
# Updates all records with details given if they match a set of conditions supplied, limits and order can
@@ -337,7 +356,7 @@ module ActiveRecord
end
end
- # Destroy an object (or multiple objects) that has the given id, the object is instantiated first,
+ # Destroy an object (or multiple objects) that has the given id. The object is instantiated first,
# therefore all callbacks and filters are fired off before the object is deleted. This method is
# less efficient than ActiveRecord#delete but allows cleanup methods and other actions to be run.
#
@@ -487,20 +506,6 @@ module ActiveRecord
end
end
- protected
-
- def method_missing(method, *args, &block)
- if Array.method_defined?(method)
- to_a.send(method, *args, &block)
- elsif @klass.respond_to?(method)
- scoping { @klass.send(method, *args, &block) }
- elsif arel.respond_to?(method)
- arel.send(method, *args, &block)
- else
- super
- end
- end
-
private
def references_eager_loaded_tables?
@@ -517,7 +522,18 @@ module ActiveRecord
# always convert table names to downcase as in Oracle quoted table names are in uppercase
joined_tables = joined_tables.flatten.compact.map { |t| t.downcase }.uniq
- (tables_in_string(to_sql) - joined_tables).any?
+ referenced_tables = (tables_in_string(to_sql) - joined_tables)
+ if referenced_tables.any?
+ ActiveSupport::Deprecation.warn(
+ "Your query appears to reference tables (#{referenced_tables.join(', ')}) that are not " \
+ "explicitly joined. This implicit joining is deprecated, so you must explicitly " \
+ "reference the tables. For example, instead of Author.includes(:posts).where(\"posts.name = 'foo'\"), " \
+ "you should write Author.eager_load(:posts).where(\"posts.name = 'foo'\")."
+ )
+ true
+ else
+ false
+ end
end
def tables_in_string(string)
@@ -526,6 +542,5 @@ module ActiveRecord
# ignore raw_sql_ that is used by Oracle adapter as alias for limit/offset subqueries
string.scan(/([a-zA-Z_][.\w]+).?\./).flatten.map{ |s| s.downcase }.uniq - ['raw_sql_']
end
-
end
end
diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb
index af86771d2d..bf9b4bf1c9 100644
--- a/activerecord/lib/active_record/relation/calculations.rb
+++ b/activerecord/lib/active_record/relation/calculations.rb
@@ -166,6 +166,22 @@ module ActiveRecord
0
end
+ # This method is designed to perform select by a single column as direct SQL query
+ # Returns <tt>Array</tt> with values of the specified column name
+ # The values has same data type as column.
+ #
+ # Examples:
+ #
+ # Person.pluck(:id) # SELECT people.id FROM people
+ # Person.uniq.pluck(:role) # SELECT DISTINCT role FROM people
+ # Person.where(:confirmed => true).limit(5).pluck(:id)
+ #
+ def pluck(column_name)
+ klass.connection.select_all(select(column_name).arel).map! do |attributes|
+ klass.type_cast_attribute(attributes.keys.first, klass.initialize_attributes(attributes))
+ end
+ end
+
private
def perform_calculation(operation, column_name, options = {})
diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb
new file mode 100644
index 0000000000..f5fdf437bf
--- /dev/null
+++ b/activerecord/lib/active_record/relation/delegation.rb
@@ -0,0 +1,49 @@
+require 'active_support/core_ext/module/delegation'
+
+module ActiveRecord
+ module Delegation
+ # Set up common delegations for performance (avoids method_missing)
+ delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to_ary, :to => :to_a
+ delegate :table_name, :quoted_table_name, :primary_key, :quoted_primary_key,
+ :connection, :columns_hash, :auto_explain_threshold_in_seconds, :to => :klass
+
+ def self.delegate_to_scoped_klass(method)
+ if method.to_s =~ /\A[a-zA-Z_]\w*[!?]?\z/
+ module_eval <<-RUBY, __FILE__, __LINE__ + 1
+ def #{method}(*args, &block)
+ scoping { @klass.#{method}(*args, &block) }
+ end
+ RUBY
+ else
+ module_eval <<-RUBY, __FILE__, __LINE__ + 1
+ def #{method}(*args, &block)
+ scoping { @klass.send(#{method.inspect}, *args, &block) }
+ end
+ RUBY
+ end
+ end
+
+ def respond_to?(method, include_private = false)
+ super || Array.method_defined?(method) ||
+ @klass.respond_to?(method, include_private) ||
+ arel.respond_to?(method, include_private)
+ end
+
+ protected
+
+ def method_missing(method, *args, &block)
+ if Array.method_defined?(method)
+ ::ActiveRecord::Delegation.delegate method, :to => :to_a
+ to_a.send(method, *args, &block)
+ elsif @klass.respond_to?(method)
+ ::ActiveRecord::Delegation.delegate_to_scoped_klass(method)
+ scoping { @klass.send(method, *args, &block) }
+ elsif arel.respond_to?(method)
+ ::ActiveRecord::Delegation.delegate method, :to => :arel
+ arel.send(method, *args, &block)
+ else
+ super
+ end
+ end
+ end
+end \ No newline at end of file
diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb
index 7eeb3dde70..e58c726e09 100644
--- a/activerecord/lib/active_record/relation/finder_methods.rb
+++ b/activerecord/lib/active_record/relation/finder_methods.rb
@@ -187,11 +187,11 @@ module ActiveRecord
def exists?(id = false)
return false if id.nil?
- id = id.id if ActiveRecord::Base === id
+ id = id.id if ActiveRecord::Model === id
join_dependency = construct_join_dependency_for_association_find
relation = construct_relation_for_association_find(join_dependency)
- relation = relation.except(:select).select("1").limit(1)
+ relation = relation.except(:select, :order).select("1").limit(1)
case id
when Array, Hash
@@ -249,7 +249,7 @@ module ActiveRecord
end
def construct_limited_ids_condition(relation)
- orders = relation.order_values.map { |val| val.presence }.compact
+ orders = (relation.reorder_value || relation.order_values).map { |val| val.presence }.compact
values = @klass.connection.distinct("#{@klass.connection.quote_table_name table_name}.#{primary_key}", orders)
relation = relation.dup
diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb
index 7e8ddd1b5d..88081edae2 100644
--- a/activerecord/lib/active_record/relation/predicate_builder.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder.rb
@@ -15,42 +15,49 @@ module ActiveRecord
table = Arel::Table.new(table_name, engine)
end
- attribute = table[column.to_sym]
+ build(table[column.to_sym], value)
+ end
+ end
+ predicates.flatten
+ end
+
+ private
+ def self.build(attribute, value)
+ case value
+ when ActiveRecord::Relation
+ value = value.select(value.klass.arel_table[value.klass.primary_key]) if value.select_values.empty?
+ attribute.in(value.arel.ast)
+ when Array, ActiveRecord::Associations::CollectionProxy
+ values = value.to_a.map {|x| x.is_a?(ActiveRecord::Model) ? x.id : x}
+ ranges, values = values.partition {|v| v.is_a?(Range) || v.is_a?(Arel::Relation)}
- case value
- when ActiveRecord::Relation
- value = value.select(value.klass.arel_table[value.klass.primary_key]) if value.select_values.empty?
- attribute.in(value.arel.ast)
- when Array, ActiveRecord::Associations::CollectionProxy
- values = value.to_a.map { |x|
- x.is_a?(ActiveRecord::Base) ? x.id : x
- }
+ array_predicates = ranges.map {|range| attribute.in(range)}
- if values.include?(nil)
- values = values.compact
- if values.empty?
- attribute.eq nil
- else
- attribute.in(values.compact).or attribute.eq(nil)
- end
+ if values.include?(nil)
+ values = values.compact
+ case values.length
+ when 0
+ array_predicates << attribute.eq(nil)
+ when 1
+ array_predicates << attribute.eq(values.first).or(attribute.eq(nil))
else
- attribute.in(values)
+ array_predicates << attribute.in(values).or(attribute.eq(nil))
end
-
- when Range, Arel::Relation
- attribute.in(value)
- when ActiveRecord::Base
- attribute.eq(value.id)
- when Class
- # FIXME: I think we need to deprecate this behavior
- attribute.eq(value.name)
else
- attribute.eq(value)
+ array_predicates << attribute.in(values)
end
+
+ array_predicates.inject {|composite, predicate| composite.or(predicate)}
+ when Range, Arel::Relation
+ attribute.in(value)
+ when ActiveRecord::Model
+ attribute.eq(value.id)
+ when Class
+ # FIXME: I think we need to deprecate this behavior
+ attribute.eq(value.name)
+ else
+ attribute.eq(value)
end
end
-
- predicates.flatten
- end
end
end
diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb
index 670ba0987d..c281bead0d 100644
--- a/activerecord/lib/active_record/relation/query_methods.rb
+++ b/activerecord/lib/active_record/relation/query_methods.rb
@@ -9,7 +9,8 @@ module ActiveRecord
:select_values, :group_values, :order_values, :joins_values,
:where_values, :having_values, :bind_values,
:limit_value, :offset_value, :lock_value, :readonly_value, :create_with_value,
- :from_value, :reorder_value, :reverse_order_value
+ :from_value, :reorder_value, :reverse_order_value,
+ :uniq_value
def includes(*args)
args.reject! {|a| a.blank? }
@@ -38,7 +39,7 @@ module ActiveRecord
end
# Works in two unique ways.
- #
+ #
# First: takes a block so it can be used just like Array#select.
#
# Model.scoped.select { |m| m.field == value }
@@ -176,9 +177,25 @@ module ActiveRecord
relation
end
+ # Specifies whether the records should be unique or not. For example:
+ #
+ # User.select(:name)
+ # # => Might return two records with the same name
+ #
+ # User.select(:name).uniq
+ # # => Returns 1 record per unique name
+ #
+ # User.select(:name).uniq.uniq(false)
+ # # => You can also remove the uniqueness
+ def uniq(value = true)
+ relation = clone
+ relation.uniq_value = value
+ relation
+ end
+
# Used to extend a scope with additional methods, either through
- # a module or through a block provided.
- #
+ # a module or through a block provided.
+ #
# The object returned is a relation, which can be further extended.
#
# === Using a module
@@ -200,7 +217,7 @@ module ActiveRecord
#
# scope = Model.scoped.extending do
# def page(number)
- # # pagination code goes here
+ # # pagination code goes here
# end
# end
# scope.page(params[:page])
@@ -209,7 +226,7 @@ module ActiveRecord
#
# scope = Model.scoped.extending(Pagination) do
# def per_page(number)
- # # pagination code goes here
+ # # pagination code goes here
# end
# end
def extending(*modules)
@@ -252,6 +269,7 @@ module ActiveRecord
build_select(arel, @select_values.uniq)
+ arel.distinct(@uniq_value)
arel.from(@from_value) if @from_value
arel.lock(@lock_value) if @lock_value
diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb
index ba882beca9..a5194beae5 100644
--- a/activerecord/lib/active_record/relation/spawn_methods.rb
+++ b/activerecord/lib/active_record/relation/spawn_methods.rb
@@ -113,7 +113,7 @@ module ActiveRecord
result
end
- VALID_FIND_OPTIONS = [ :conditions, :include, :joins, :limit, :offset, :extend,
+ VALID_FIND_OPTIONS = [ :conditions, :include, :joins, :limit, :offset, :extend, :eager_load,
:order, :select, :readonly, :group, :having, :from, :lock ]
def apply_finder_options(options)
@@ -124,7 +124,7 @@ module ActiveRecord
finders = options.dup
finders.delete_if { |key, value| value.nil? && key != :limit }
- ([:joins, :select, :group, :order, :having, :limit, :offset, :from, :lock, :readonly] & finders.keys).each do |finder|
+ ([:joins, :select, :group, :order, :having, :limit, :offset, :from, :lock, :readonly, :eager_load] & finders.keys).each do |finder|
relation = relation.send(finder, finders[finder])
end
diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb
new file mode 100644
index 0000000000..2d7d83d160
--- /dev/null
+++ b/activerecord/lib/active_record/sanitization.rb
@@ -0,0 +1,194 @@
+require 'active_support/concern'
+
+module ActiveRecord
+ module Sanitization
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ def quote_value(value, column = nil) #:nodoc:
+ connection.quote(value,column)
+ end
+
+ # Used to sanitize objects before they're used in an SQL SELECT statement. Delegates to <tt>connection.quote</tt>.
+ def sanitize(object) #:nodoc:
+ connection.quote(object)
+ end
+
+ protected
+
+ # Accepts an array, hash, or string of SQL conditions and sanitizes
+ # them into a valid SQL fragment for a WHERE clause.
+ # ["name='%s' and group_id='%s'", "foo'bar", 4] returns "name='foo''bar' and group_id='4'"
+ # { :name => "foo'bar", :group_id => 4 } returns "name='foo''bar' and group_id='4'"
+ # "name='foo''bar' and group_id='4'" returns "name='foo''bar' and group_id='4'"
+ def sanitize_sql_for_conditions(condition, table_name = self.table_name)
+ return nil if condition.blank?
+
+ case condition
+ when Array; sanitize_sql_array(condition)
+ when Hash; sanitize_sql_hash_for_conditions(condition, table_name)
+ else condition
+ end
+ end
+ alias_method :sanitize_sql, :sanitize_sql_for_conditions
+
+ # Accepts an array, hash, or string of SQL conditions and sanitizes
+ # them into a valid SQL fragment for a SET clause.
+ # { :name => nil, :group_id => 4 } returns "name = NULL , group_id='4'"
+ def sanitize_sql_for_assignment(assignments)
+ case assignments
+ when Array; sanitize_sql_array(assignments)
+ when Hash; sanitize_sql_hash_for_assignment(assignments)
+ else assignments
+ end
+ end
+
+ # Accepts a hash of SQL conditions and replaces those attributes
+ # that correspond to a +composed_of+ relationship with their expanded
+ # aggregate attribute values.
+ # Given:
+ # class Person < ActiveRecord::Base
+ # composed_of :address, :class_name => "Address",
+ # :mapping => [%w(address_street street), %w(address_city city)]
+ # end
+ # Then:
+ # { :address => Address.new("813 abc st.", "chicago") }
+ # # => { :address_street => "813 abc st.", :address_city => "chicago" }
+ def expand_hash_conditions_for_aggregates(attrs)
+ expanded_attrs = {}
+ attrs.each do |attr, value|
+ unless (aggregation = reflect_on_aggregation(attr.to_sym)).nil?
+ mapping = aggregate_mapping(aggregation)
+ mapping.each do |field_attr, aggregate_attr|
+ if mapping.size == 1 && !value.respond_to?(aggregate_attr)
+ expanded_attrs[field_attr] = value
+ else
+ expanded_attrs[field_attr] = value.send(aggregate_attr)
+ end
+ end
+ else
+ expanded_attrs[attr] = value
+ end
+ end
+ expanded_attrs
+ end
+
+ # Sanitizes a hash of attribute/value pairs into SQL conditions for a WHERE clause.
+ # { :name => "foo'bar", :group_id => 4 }
+ # # => "name='foo''bar' and group_id= 4"
+ # { :status => nil, :group_id => [1,2,3] }
+ # # => "status IS NULL and group_id IN (1,2,3)"
+ # { :age => 13..18 }
+ # # => "age BETWEEN 13 AND 18"
+ # { 'other_records.id' => 7 }
+ # # => "`other_records`.`id` = 7"
+ # { :other_records => { :id => 7 } }
+ # # => "`other_records`.`id` = 7"
+ # And for value objects on a composed_of relationship:
+ # { :address => Address.new("123 abc st.", "chicago") }
+ # # => "address_street='123 abc st.' and address_city='chicago'"
+ def sanitize_sql_hash_for_conditions(attrs, default_table_name = self.table_name)
+ attrs = expand_hash_conditions_for_aggregates(attrs)
+
+ table = Arel::Table.new(table_name).alias(default_table_name)
+ PredicateBuilder.build_from_hash(arel_engine, attrs, table).map { |b|
+ connection.visitor.accept b
+ }.join(' AND ')
+ end
+ alias_method :sanitize_sql_hash, :sanitize_sql_hash_for_conditions
+
+ # Sanitizes a hash of attribute/value pairs into SQL conditions for a SET clause.
+ # { :status => nil, :group_id => 1 }
+ # # => "status = NULL , group_id = 1"
+ def sanitize_sql_hash_for_assignment(attrs)
+ attrs.map do |attr, value|
+ "#{connection.quote_column_name(attr)} = #{quote_bound_value(value)}"
+ end.join(', ')
+ end
+
+ # Accepts an array of conditions. The array has each value
+ # sanitized and interpolated into the SQL statement.
+ # ["name='%s' and group_id='%s'", "foo'bar", 4] returns "name='foo''bar' and group_id='4'"
+ def sanitize_sql_array(ary)
+ statement, *values = ary
+ if values.first.is_a?(Hash) && statement =~ /:\w+/
+ replace_named_bind_variables(statement, values.first)
+ elsif statement.include?('?')
+ replace_bind_variables(statement, values)
+ elsif statement.blank?
+ statement
+ else
+ statement % values.collect { |value| connection.quote_string(value.to_s) }
+ end
+ end
+
+ alias_method :sanitize_conditions, :sanitize_sql
+
+ def replace_bind_variables(statement, values) #:nodoc:
+ raise_if_bind_arity_mismatch(statement, statement.count('?'), values.size)
+ bound = values.dup
+ c = connection
+ statement.gsub('?') { quote_bound_value(bound.shift, c) }
+ end
+
+ def replace_named_bind_variables(statement, bind_vars) #:nodoc:
+ statement.gsub(/(:?):([a-zA-Z]\w*)/) do
+ if $1 == ':' # skip postgresql casts
+ $& # return the whole match
+ elsif bind_vars.include?(match = $2.to_sym)
+ quote_bound_value(bind_vars[match])
+ else
+ raise PreparedStatementInvalid, "missing value for :#{match} in #{statement}"
+ end
+ end
+ end
+
+ def expand_range_bind_variables(bind_vars) #:nodoc:
+ expanded = []
+
+ bind_vars.each do |var|
+ next if var.is_a?(Hash)
+
+ if var.is_a?(Range)
+ expanded << var.first
+ expanded << var.last
+ else
+ expanded << var
+ end
+ end
+
+ expanded
+ end
+
+ def quote_bound_value(value, c = connection) #:nodoc:
+ if value.respond_to?(:map) && !value.acts_like?(:string)
+ if value.respond_to?(:empty?) && value.empty?
+ c.quote(nil)
+ else
+ value.map { |v| c.quote(v) }.join(',')
+ end
+ else
+ c.quote(value)
+ end
+ end
+
+ def raise_if_bind_arity_mismatch(statement, expected, provided) #:nodoc:
+ unless expected == provided
+ raise PreparedStatementInvalid, "wrong number of bind variables (#{provided} for #{expected}) in: #{statement}"
+ end
+ end
+ end
+
+ # TODO: Deprecate this
+ def quoted_id #:nodoc:
+ quote_value(id, column_for_attribute(self.class.primary_key))
+ end
+
+ private
+
+ # Quote strings appropriately for SQL statements.
+ def quote_value(value, column = nil)
+ self.class.connection.quote(value, column)
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb
index 6fe305f843..cdde5cf3b9 100644
--- a/activerecord/lib/active_record/schema_dumper.rb
+++ b/activerecord/lib/active_record/schema_dumper.rb
@@ -190,6 +190,9 @@ HEADER
index_lengths = (index.lengths || []).compact
statement_parts << (':length => ' + Hash[index.columns.zip(index.lengths)].inspect) unless index_lengths.empty?
+ index_orders = (index.orders || {})
+ statement_parts << (':order => ' + index.orders.inspect) unless index_orders.empty?
+
' ' + statement_parts.join(', ')
end
diff --git a/activerecord/lib/active_record/scoping.rb b/activerecord/lib/active_record/scoping.rb
new file mode 100644
index 0000000000..a8f5e96190
--- /dev/null
+++ b/activerecord/lib/active_record/scoping.rb
@@ -0,0 +1,152 @@
+require 'active_support/concern'
+
+module ActiveRecord
+ module Scoping
+ extend ActiveSupport::Concern
+
+ included do
+ include Default
+ include Named
+ end
+
+ module ClassMethods
+ # with_scope lets you apply options to inner block incrementally. It takes a hash and the keys must be
+ # <tt>:find</tt> or <tt>:create</tt>. <tt>:find</tt> parameter is <tt>Relation</tt> while
+ # <tt>:create</tt> parameters are an attributes hash.
+ #
+ # class Article < ActiveRecord::Base
+ # def self.create_with_scope
+ # with_scope(:find => where(:blog_id => 1), :create => { :blog_id => 1 }) do
+ # find(1) # => SELECT * from articles WHERE blog_id = 1 AND id = 1
+ # a = create(1)
+ # a.blog_id # => 1
+ # end
+ # end
+ # end
+ #
+ # In nested scopings, all previous parameters are overwritten by the innermost rule, with the exception of
+ # <tt>where</tt>, <tt>includes</tt>, and <tt>joins</tt> operations in <tt>Relation</tt>, which are merged.
+ #
+ # <tt>joins</tt> operations are uniqued so multiple scopes can join in the same table without table aliasing
+ # problems. If you need to join multiple tables, but still want one of the tables to be uniqued, use the
+ # array of strings format for your joins.
+ #
+ # class Article < ActiveRecord::Base
+ # def self.find_with_scope
+ # with_scope(:find => where(:blog_id => 1).limit(1), :create => { :blog_id => 1 }) do
+ # with_scope(:find => limit(10)) do
+ # all # => SELECT * from articles WHERE blog_id = 1 LIMIT 10
+ # end
+ # with_scope(:find => where(:author_id => 3)) do
+ # all # => SELECT * from articles WHERE blog_id = 1 AND author_id = 3 LIMIT 1
+ # end
+ # end
+ # end
+ # end
+ #
+ # You can ignore any previous scopings by using the <tt>with_exclusive_scope</tt> method.
+ #
+ # class Article < ActiveRecord::Base
+ # def self.find_with_exclusive_scope
+ # with_scope(:find => where(:blog_id => 1).limit(1)) do
+ # with_exclusive_scope(:find => limit(10)) do
+ # all # => SELECT * from articles LIMIT 10
+ # end
+ # end
+ # end
+ # end
+ #
+ # *Note*: the +:find+ scope also has effect on update and deletion methods, like +update_all+ and +delete_all+.
+ def with_scope(scope = {}, action = :merge, &block)
+ # If another Active Record class has been passed in, get its current scope
+ scope = scope.current_scope if !scope.is_a?(Relation) && scope.respond_to?(:current_scope)
+
+ previous_scope = self.current_scope
+
+ if scope.is_a?(Hash)
+ # Dup first and second level of hash (method and params).
+ scope = scope.dup
+ scope.each do |method, params|
+ scope[method] = params.dup unless params == true
+ end
+
+ scope.assert_valid_keys([ :find, :create ])
+ relation = construct_finder_arel(scope[:find] || {})
+ relation.default_scoped = true unless action == :overwrite
+
+ if previous_scope && previous_scope.create_with_value && scope[:create]
+ scope_for_create = if action == :merge
+ previous_scope.create_with_value.merge(scope[:create])
+ else
+ scope[:create]
+ end
+
+ relation = relation.create_with(scope_for_create)
+ else
+ scope_for_create = scope[:create]
+ scope_for_create ||= previous_scope.create_with_value if previous_scope
+ relation = relation.create_with(scope_for_create) if scope_for_create
+ end
+
+ scope = relation
+ end
+
+ scope = previous_scope.merge(scope) if previous_scope && action == :merge
+
+ self.current_scope = scope
+ begin
+ yield
+ ensure
+ self.current_scope = previous_scope
+ end
+ end
+
+ protected
+
+ # Works like with_scope, but discards any nested properties.
+ def with_exclusive_scope(method_scoping = {}, &block)
+ if method_scoping.values.any? { |e| e.is_a?(ActiveRecord::Relation) }
+ raise ArgumentError, <<-MSG
+ New finder API can not be used with_exclusive_scope. You can either call unscoped to get an anonymous scope not bound to the default_scope:
+
+ User.unscoped.where(:active => true)
+
+ Or call unscoped with a block:
+
+ User.unscoped do
+ User.where(:active => true).all
+ end
+
+ MSG
+ end
+ with_scope(method_scoping, :overwrite, &block)
+ end
+
+ def current_scope #:nodoc:
+ Thread.current["#{self}_current_scope"]
+ end
+
+ def current_scope=(scope) #:nodoc:
+ Thread.current["#{self}_current_scope"] = scope
+ end
+
+ private
+
+ def construct_finder_arel(options = {}, scope = nil)
+ relation = options.is_a?(Hash) ? unscoped.apply_finder_options(options) : options
+ relation = scope.merge(relation) if scope
+ relation
+ end
+
+ end
+
+ def populate_with_current_scope_attributes
+ return unless self.class.scope_attributes?
+
+ self.class.scope_attributes.each do |att,value|
+ send("#{att}=", value) if respond_to?("#{att}=")
+ end
+ end
+
+ end
+end
diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb
new file mode 100644
index 0000000000..5f05d146f2
--- /dev/null
+++ b/activerecord/lib/active_record/scoping/default.rb
@@ -0,0 +1,142 @@
+require 'active_support/concern'
+
+module ActiveRecord
+ module Scoping
+ module Default
+ extend ActiveSupport::Concern
+
+ included do
+ # Stores the default scope for the class
+ config_attribute :default_scopes
+ self.default_scopes = []
+ end
+
+ module ClassMethods
+ # Returns a scope for the model without the default_scope.
+ #
+ # class Post < ActiveRecord::Base
+ # def self.default_scope
+ # where :published => true
+ # end
+ # end
+ #
+ # Post.all # Fires "SELECT * FROM posts WHERE published = true"
+ # Post.unscoped.all # Fires "SELECT * FROM posts"
+ #
+ # This method also accepts a block. All queries inside the block will
+ # not use the default_scope:
+ #
+ # Post.unscoped {
+ # Post.limit(10) # Fires "SELECT * FROM posts LIMIT 10"
+ # }
+ #
+ # It is recommended to use the block form of unscoped because chaining
+ # unscoped with <tt>scope</tt> does not work. Assuming that
+ # <tt>published</tt> is a <tt>scope</tt>, the following two statements
+ # are equal: the default_scope is applied on both.
+ #
+ # Post.unscoped.published
+ # Post.published
+ def unscoped #:nodoc:
+ block_given? ? relation.scoping { yield } : relation
+ end
+
+ def before_remove_const #:nodoc:
+ self.current_scope = nil
+ end
+
+ protected
+
+ # Use this macro in your model to set a default scope for all operations on
+ # the model.
+ #
+ # class Article < ActiveRecord::Base
+ # default_scope where(:published => true)
+ # end
+ #
+ # Article.all # => SELECT * FROM articles WHERE published = true
+ #
+ # The <tt>default_scope</tt> is also applied while creating/building a record. It is not
+ # applied while updating a record.
+ #
+ # Article.new.published # => true
+ # Article.create.published # => true
+ #
+ # You can also use <tt>default_scope</tt> with a block, in order to have it lazily evaluated:
+ #
+ # class Article < ActiveRecord::Base
+ # default_scope { where(:published_at => Time.now - 1.week) }
+ # end
+ #
+ # (You can also pass any object which responds to <tt>call</tt> to the <tt>default_scope</tt>
+ # macro, and it will be called when building the default scope.)
+ #
+ # If you use multiple <tt>default_scope</tt> declarations in your model then they will
+ # be merged together:
+ #
+ # class Article < ActiveRecord::Base
+ # default_scope where(:published => true)
+ # default_scope where(:rating => 'G')
+ # end
+ #
+ # Article.all # => SELECT * FROM articles WHERE published = true AND rating = 'G'
+ #
+ # This is also the case with inheritance and module includes where the parent or module
+ # defines a <tt>default_scope</tt> and the child or including class defines a second one.
+ #
+ # If you need to do more complex things with a default scope, you can alternatively
+ # define it as a class method:
+ #
+ # class Article < ActiveRecord::Base
+ # def self.default_scope
+ # # Should return a scope, you can call 'super' here etc.
+ # end
+ # end
+ def default_scope(scope = {})
+ scope = Proc.new if block_given?
+ self.default_scopes = default_scopes + [scope]
+ end
+
+ def build_default_scope #:nodoc:
+ if method(:default_scope).owner != ActiveRecord::Scoping::Default::ClassMethods
+ evaluate_default_scope { default_scope }
+ elsif default_scopes.any?
+ evaluate_default_scope do
+ default_scopes.inject(relation) do |default_scope, scope|
+ if scope.is_a?(Hash)
+ default_scope.apply_finder_options(scope)
+ elsif !scope.is_a?(Relation) && scope.respond_to?(:call)
+ default_scope.merge(scope.call)
+ else
+ default_scope.merge(scope)
+ end
+ end
+ end
+ end
+ end
+
+ def ignore_default_scope? #:nodoc:
+ Thread.current["#{self}_ignore_default_scope"]
+ end
+
+ def ignore_default_scope=(ignore) #:nodoc:
+ Thread.current["#{self}_ignore_default_scope"] = ignore
+ end
+
+ # The ignore_default_scope flag is used to prevent an infinite recursion situation where
+ # a default scope references a scope which has a default scope which references a scope...
+ def evaluate_default_scope
+ return if ignore_default_scope?
+
+ begin
+ self.ignore_default_scope = true
+ yield
+ ensure
+ self.ignore_default_scope = false
+ end
+ end
+
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb
new file mode 100644
index 0000000000..0edc3f1dcc
--- /dev/null
+++ b/activerecord/lib/active_record/scoping/named.rb
@@ -0,0 +1,202 @@
+require 'active_support/core_ext/array'
+require 'active_support/core_ext/hash/except'
+require 'active_support/core_ext/kernel/singleton_class'
+require 'active_support/core_ext/object/blank'
+require 'active_support/core_ext/class/attribute'
+
+module ActiveRecord
+ # = Active Record Named \Scopes
+ module Scoping
+ module Named
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ # Returns an anonymous \scope.
+ #
+ # posts = Post.scoped
+ # posts.size # Fires "select count(*) from posts" and returns the count
+ # posts.each {|p| puts p.name } # Fires "select * from posts" and loads post objects
+ #
+ # fruits = Fruit.scoped
+ # fruits = fruits.where(:color => 'red') if options[:red_only]
+ # fruits = fruits.limit(10) if limited?
+ #
+ # Anonymous \scopes tend to be useful when procedurally generating complex
+ # queries, where passing intermediate values (\scopes) around as first-class
+ # objects is convenient.
+ #
+ # You can define a \scope that applies to all finders using
+ # ActiveRecord::Base.default_scope.
+ def scoped(options = nil)
+ if options
+ scoped.apply_finder_options(options)
+ else
+ if current_scope
+ current_scope.clone
+ else
+ scope = relation.clone
+ scope.default_scoped = true
+ scope
+ end
+ end
+ end
+
+ ##
+ # Collects attributes from scopes that should be applied when creating
+ # an AR instance for the particular class this is called on.
+ def scope_attributes # :nodoc:
+ if current_scope
+ current_scope.scope_for_create
+ else
+ scope = relation.clone
+ scope.default_scoped = true
+ scope.scope_for_create
+ end
+ end
+
+ ##
+ # Are there default attributes associated with this scope?
+ def scope_attributes? # :nodoc:
+ current_scope || default_scopes.any?
+ end
+
+ # Adds a class method for retrieving and querying objects. A \scope represents a narrowing of a database query,
+ # such as <tt>where(:color => :red).select('shirts.*').includes(:washing_instructions)</tt>.
+ #
+ # class Shirt < ActiveRecord::Base
+ # scope :red, where(:color => 'red')
+ # scope :dry_clean_only, joins(:washing_instructions).where('washing_instructions.dry_clean_only = ?', true)
+ # end
+ #
+ # The above calls to <tt>scope</tt> define class methods Shirt.red and Shirt.dry_clean_only. Shirt.red,
+ # in effect, represents the query <tt>Shirt.where(:color => 'red')</tt>.
+ #
+ # Note that this is simply 'syntactic sugar' for defining an actual class method:
+ #
+ # class Shirt < ActiveRecord::Base
+ # def self.red
+ # where(:color => 'red')
+ # end
+ # end
+ #
+ # Unlike <tt>Shirt.find(...)</tt>, however, the object returned by Shirt.red is not an Array; it
+ # resembles the association object constructed by a <tt>has_many</tt> declaration. For instance,
+ # you can invoke <tt>Shirt.red.first</tt>, <tt>Shirt.red.count</tt>, <tt>Shirt.red.where(:size => 'small')</tt>.
+ # Also, just as with the association objects, named \scopes act like an Array, implementing Enumerable;
+ # <tt>Shirt.red.each(&block)</tt>, <tt>Shirt.red.first</tt>, and <tt>Shirt.red.inject(memo, &block)</tt>
+ # all behave as if Shirt.red really was an Array.
+ #
+ # These named \scopes are composable. For instance, <tt>Shirt.red.dry_clean_only</tt> will produce
+ # all shirts that are both red and dry clean only.
+ # Nested finds and calculations also work with these compositions: <tt>Shirt.red.dry_clean_only.count</tt>
+ # returns the number of garments for which these criteria obtain. Similarly with
+ # <tt>Shirt.red.dry_clean_only.average(:thread_count)</tt>.
+ #
+ # All \scopes are available as class methods on the ActiveRecord::Base descendant upon which
+ # the \scopes were defined. But they are also available to <tt>has_many</tt> associations. If,
+ #
+ # class Person < ActiveRecord::Base
+ # has_many :shirts
+ # end
+ #
+ # then <tt>elton.shirts.red.dry_clean_only</tt> will return all of Elton's red, dry clean
+ # only shirts.
+ #
+ # Named \scopes can also be procedural:
+ #
+ # class Shirt < ActiveRecord::Base
+ # scope :colored, lambda { |color| where(:color => color) }
+ # end
+ #
+ # In this example, <tt>Shirt.colored('puce')</tt> finds all puce shirts.
+ #
+ # On Ruby 1.9 you can use the 'stabby lambda' syntax:
+ #
+ # scope :colored, ->(color) { where(:color => color) }
+ #
+ # Note that scopes defined with \scope will be evaluated when they are defined, rather than
+ # when they are used. For example, the following would be incorrect:
+ #
+ # class Post < ActiveRecord::Base
+ # scope :recent, where('published_at >= ?', Time.current - 1.week)
+ # end
+ #
+ # The example above would be 'frozen' to the <tt>Time.current</tt> value when the <tt>Post</tt>
+ # class was defined, and so the resultant SQL query would always be the same. The correct
+ # way to do this would be via a lambda, which will re-evaluate the scope each time
+ # it is called:
+ #
+ # class Post < ActiveRecord::Base
+ # scope :recent, lambda { where('published_at >= ?', Time.current - 1.week) }
+ # end
+ #
+ # Named \scopes can also have extensions, just as with <tt>has_many</tt> declarations:
+ #
+ # class Shirt < ActiveRecord::Base
+ # scope :red, where(:color => 'red') do
+ # def dom_id
+ # 'red_shirts'
+ # end
+ # end
+ # end
+ #
+ # Scopes can also be used while creating/building a record.
+ #
+ # class Article < ActiveRecord::Base
+ # scope :published, where(:published => true)
+ # end
+ #
+ # Article.published.new.published # => true
+ # Article.published.create.published # => true
+ #
+ # Class methods on your model are automatically available
+ # on scopes. Assuming the following setup:
+ #
+ # class Article < ActiveRecord::Base
+ # scope :published, where(:published => true)
+ # scope :featured, where(:featured => true)
+ #
+ # def self.latest_article
+ # order('published_at desc').first
+ # end
+ #
+ # def self.titles
+ # map(&:title)
+ # end
+ #
+ # end
+ #
+ # We are able to call the methods like this:
+ #
+ # Article.published.featured.latest_article
+ # Article.featured.titles
+
+ def scope(name, scope_options = {})
+ name = name.to_sym
+ valid_scope_name?(name)
+ extension = Module.new(&Proc.new) if block_given?
+
+ scope_proc = lambda do |*args|
+ options = scope_options.respond_to?(:call) ? unscoped { scope_options.call(*args) } : scope_options
+ options = scoped.apply_finder_options(options) if options.is_a?(Hash)
+
+ relation = scoped.merge(options)
+
+ extension ? relation.extending(extension) : relation
+ end
+
+ singleton_class.send(:redefine_method, name, &scope_proc)
+ end
+
+ protected
+
+ def valid_scope_name?(name)
+ if respond_to?(name, true)
+ logger.warn "Creating scope :#{name}. " \
+ "Overwriting existing method #{self.name}.#{name}."
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/session_store.rb b/activerecord/lib/active_record/session_store.rb
index 76c37cc367..e3bbd06f7e 100644
--- a/activerecord/lib/active_record/session_store.rb
+++ b/activerecord/lib/active_record/session_store.rb
@@ -59,12 +59,12 @@ module ActiveRecord
end
def drop_table!
- connection_pool.clear_table_cache!(table_name)
+ connection.schema_cache.clear_table_cache!(table_name)
connection.drop_table table_name
end
def create_table!
- connection_pool.clear_table_cache!(table_name)
+ connection.schema_cache.clear_table_cache!(table_name)
connection.create_table(table_name) do |t|
t.string session_id_column, :limit => 255
t.text data_column_name
diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb
new file mode 100644
index 0000000000..8cc84f81d0
--- /dev/null
+++ b/activerecord/lib/active_record/store.rb
@@ -0,0 +1,50 @@
+module ActiveRecord
+ # Store gives you a thin wrapper around serialize for the purpose of storing hashes in a single column.
+ # It's like a simple key/value store backed into your record when you don't care about being able to
+ # query that store outside the context of a single record.
+ #
+ # You can then declare accessors to this store that are then accessible just like any other attribute
+ # of the model. This is very helpful for easily exposing store keys to a form or elsewhere that's
+ # already built around just accessing attributes on the model.
+ #
+ # Make sure that you declare the database column used for the serialized store as a text, so there's
+ # plenty of room.
+ #
+ # Examples:
+ #
+ # class User < ActiveRecord::Base
+ # store :settings, accessors: [ :color, :homepage ]
+ # end
+ #
+ # u = User.new(color: 'black', homepage: '37signals.com')
+ # u.color # Accessor stored attribute
+ # u.settings[:country] = 'Denmark' # Any attribute, even if not specified with an accessor
+ #
+ # # Add additional accessors to an existing store through store_accessor
+ # class SuperUser < User
+ # store_accessor :settings, :privileges, :servants
+ # end
+ module Store
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ def store(store_attribute, options = {})
+ serialize store_attribute, Hash
+ store_accessor(store_attribute, options[:accessors]) if options.has_key? :accessors
+ end
+
+ def store_accessor(store_attribute, *keys)
+ Array(keys).flatten.each do |key|
+ define_method("#{key}=") do |value|
+ send(store_attribute)[key] = value
+ send("#{store_attribute}_will_change!")
+ end
+
+ define_method(key) do
+ send(store_attribute)[key]
+ end
+ end
+ end
+ end
+ end
+end \ No newline at end of file
diff --git a/activerecord/lib/active_record/test_case.rb b/activerecord/lib/active_record/test_case.rb
index ffe9b08dce..5398a14fc6 100644
--- a/activerecord/lib/active_record/test_case.rb
+++ b/activerecord/lib/active_record/test_case.rb
@@ -13,13 +13,6 @@ module ActiveRecord
ActiveRecord::IdentityMap.clear
end
- # Backport skip to Ruby 1.8. test/unit doesn't support it, so just
- # make it a noop.
- unless instance_methods.map(&:to_s).include?("skip")
- def skip(message)
- end
- end
-
def assert_date_from_db(expected, actual, message = nil)
# SybaseAdapter doesn't have a separate column type just for dates,
# so the time is in the string and incorrectly formatted
@@ -56,18 +49,5 @@ module ActiveRecord
ensure
ActiveRecord::SQLCounter.ignored_sql = prev_ignored_sql
end
-
- def with_kcode(kcode)
- if RUBY_VERSION < '1.9'
- orig_kcode, $KCODE = $KCODE, kcode
- begin
- yield
- ensure
- $KCODE = orig_kcode
- end
- else
- yield
- end
- end
end
end
diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb
index 4d5e469a7f..c717fdea47 100644
--- a/activerecord/lib/active_record/timestamp.rb
+++ b/activerecord/lib/active_record/timestamp.rb
@@ -33,7 +33,7 @@ module ActiveRecord
extend ActiveSupport::Concern
included do
- class_attribute :record_timestamps, :instance_writer => false
+ config_attribute :record_timestamps, :instance_writer => true
self.record_timestamps = true
end
diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb
index d4870dd3f2..2c70d31b94 100644
--- a/activerecord/lib/active_record/transactions.rb
+++ b/activerecord/lib/active_record/transactions.rb
@@ -301,9 +301,9 @@ module ActiveRecord
protected
# Save the new record state and id of a record so it can be restored later if a transaction fails.
- def remember_transaction_record_state #:nodoc
+ def remember_transaction_record_state #:nodoc:
@_start_transaction_state ||= {}
- @_start_transaction_state[:id] = id if self.class.primary_key?
+ @_start_transaction_state[:id] = id if has_attribute?(self.class.primary_key)
unless @_start_transaction_state.include?(:new_record)
@_start_transaction_state[:new_record] = @new_record
end
@@ -314,7 +314,7 @@ module ActiveRecord
end
# Clear the new record state and id of a record.
- def clear_transaction_record_state #:nodoc
+ def clear_transaction_record_state #:nodoc:
if defined?(@_start_transaction_state)
@_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1
remove_instance_variable(:@_start_transaction_state) if @_start_transaction_state[:level] < 1
@@ -322,7 +322,7 @@ module ActiveRecord
end
# Restore the new record state and id of a record that was previously saved by a call to save_record_state.
- def restore_transaction_record_state(force = false) #:nodoc
+ def restore_transaction_record_state(force = false) #:nodoc:
if defined?(@_start_transaction_state)
@_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1
if @_start_transaction_state[:level] < 1
@@ -341,12 +341,12 @@ module ActiveRecord
end
# Determine if a record was created or destroyed in a transaction. State should be one of :new_record or :destroyed.
- def transaction_record_state(state) #:nodoc
+ def transaction_record_state(state) #:nodoc:
@_start_transaction_state[state] if defined?(@_start_transaction_state)
end
# Determine if a transaction included an action for :create, :update, or :destroy. Used in filtering callbacks.
- def transaction_include_action?(action) #:nodoc
+ def transaction_include_action?(action) #:nodoc:
case action
when :create
transaction_record_state(:new_record)
diff --git a/activerecord/lib/active_record/translation.rb b/activerecord/lib/active_record/translation.rb
new file mode 100644
index 0000000000..ddcb5f2a7a
--- /dev/null
+++ b/activerecord/lib/active_record/translation.rb
@@ -0,0 +1,22 @@
+module ActiveRecord
+ module Translation
+ include ActiveModel::Translation
+
+ # Set the lookup ancestors for ActiveModel.
+ def lookup_ancestors #:nodoc:
+ klass = self
+ classes = [klass]
+ return classes if klass == ActiveRecord::Base
+
+ while klass != klass.base_class
+ classes << klass = klass.superclass
+ end
+ classes
+ end
+
+ # Set the i18n scope to overwrite ActiveModel.
+ def i18n_scope #:nodoc:
+ :activerecord
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/validations/associated.rb b/activerecord/lib/active_record/validations/associated.rb
index 7af0352a31..9f072c4c39 100644
--- a/activerecord/lib/active_record/validations/associated.rb
+++ b/activerecord/lib/active_record/validations/associated.rb
@@ -2,8 +2,9 @@ module ActiveRecord
module Validations
class AssociatedValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
- return if (value.is_a?(Array) ? value : [value]).collect{ |r| r.nil? || r.valid? }.all?
- record.errors.add(attribute, :invalid, options.merge(:value => value))
+ if Array.wrap(value).reject {|r| r.marked_for_destruction? || r.valid?}.any?
+ record.errors.add(attribute, :invalid, options.merge(:value => value))
+ end
end
end
diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb
index 484b1d369b..2e2ea8c42b 100644
--- a/activerecord/lib/active_record/validations/uniqueness.rb
+++ b/activerecord/lib/active_record/validations/uniqueness.rb
@@ -57,8 +57,8 @@ module ActiveRecord
value = column.limit ? value.to_s.mb_chars[0, column.limit] : value.to_s if column.text?
if !options[:case_sensitive] && value && column.text?
- # will use SQL LOWER function before comparison
- relation = table[attribute].lower.eq(table.lower(value))
+ # will use SQL LOWER function before comparison, unless it detects a case insensitive collation
+ relation = klass.connection.case_insensitive_comparison(table, attribute, column, value)
else
value = klass.connection.case_sensitive_modifier(value)
relation = table[attribute].eq(value)
diff --git a/activerecord/lib/active_record/version.rb b/activerecord/lib/active_record/version.rb
index 838aa8fb1e..0c35adc11d 100644
--- a/activerecord/lib/active_record/version.rb
+++ b/activerecord/lib/active_record/version.rb
@@ -1,7 +1,7 @@
module ActiveRecord
module VERSION #:nodoc:
- MAJOR = 3
- MINOR = 2
+ MAJOR = 4
+ MINOR = 0
TINY = 0
PRE = "beta"