aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord/lib')
-rw-r--r--activerecord/lib/active_record.rb4
-rw-r--r--activerecord/lib/active_record/association_preload.rb53
-rwxr-xr-xactiverecord/lib/active_record/associations.rb97
-rw-r--r--activerecord/lib/active_record/associations/association_collection.rb6
-rw-r--r--activerecord/lib/active_record/associations/association_proxy.rb5
-rw-r--r--activerecord/lib/active_record/associations/has_many_through_association.rb6
-rwxr-xr-xactiverecord/lib/active_record/base.rb103
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb1
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb12
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb52
-rwxr-xr-xactiverecord/lib/active_record/connection_adapters/abstract_adapter.rb10
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql_adapter.rb41
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb14
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb4
-rw-r--r--activerecord/lib/active_record/dirty.rb2
-rw-r--r--activerecord/lib/active_record/dynamic_scope_match.rb25
-rw-r--r--activerecord/lib/active_record/query_cache.rb38
-rw-r--r--activerecord/lib/active_record/serializers/xml_serializer.rb23
-rw-r--r--activerecord/lib/active_record/session_store.rb319
-rw-r--r--activerecord/lib/active_record/timestamp.rb4
-rw-r--r--activerecord/lib/active_record/transactions.rb4
-rw-r--r--activerecord/lib/active_record/validations.rb7
22 files changed, 667 insertions, 163 deletions
diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb
index 348e5b94af..390c005785 100644
--- a/activerecord/lib/active_record.rb
+++ b/activerecord/lib/active_record.rb
@@ -37,6 +37,8 @@ module ActiveRecord
[Base, DynamicFinderMatch, ConnectionAdapters::AbstractAdapter]
end
+ autoload :VERSION, 'active_record/version'
+
autoload :ActiveRecordError, 'active_record/base'
autoload :ConnectionNotEstablished, 'active_record/base'
@@ -49,6 +51,7 @@ module ActiveRecord
autoload :Callbacks, 'active_record/callbacks'
autoload :Dirty, 'active_record/dirty'
autoload :DynamicFinderMatch, 'active_record/dynamic_finder_match'
+ autoload :DynamicScopeMatch, 'active_record/dynamic_scope_match'
autoload :Migration, 'active_record/migration'
autoload :Migrator, 'active_record/migration'
autoload :NamedScope, 'active_record/named_scope'
@@ -58,6 +61,7 @@ module ActiveRecord
autoload :Schema, 'active_record/schema'
autoload :SchemaDumper, 'active_record/schema_dumper'
autoload :Serialization, 'active_record/serialization'
+ autoload :SessionStore, 'active_record/session_store'
autoload :TestCase, 'active_record/test_case'
autoload :Timestamp, 'active_record/timestamp'
autoload :Transactions, 'active_record/transactions'
diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb
index 99c3ce5e62..e4ab69aa1b 100644
--- a/activerecord/lib/active_record/association_preload.rb
+++ b/activerecord/lib/active_record/association_preload.rb
@@ -43,7 +43,7 @@ module ActiveRecord
# loading in a more high-level (application developer-friendly) manner.
module ClassMethods
protected
-
+
# Eager loads the named associations for the given ActiveRecord record(s).
#
# In this description, 'association name' shall refer to the name passed
@@ -94,8 +94,8 @@ module ActiveRecord
raise "parent must be an association name" unless parent.is_a?(String) || parent.is_a?(Symbol)
preload_associations(records, parent, preload_options)
reflection = reflections[parent]
- parents = records.map {|record| record.send(reflection.name)}.flatten
- unless parents.empty? || parents.first.nil?
+ parents = records.map {|record| record.send(reflection.name)}.flatten.compact
+ unless parents.empty?
parents.first.class.preload_associations(parents, child)
end
end
@@ -113,7 +113,7 @@ module ActiveRecord
# unnecessarily
records.group_by {|record| class_to_reflection[record.class] ||= record.class.reflections[association]}.each do |reflection, records|
raise ConfigurationError, "Association named '#{ association }' was not found; perhaps you misspelled it?" unless reflection
-
+
# 'reflection.macro' can return 'belongs_to', 'has_many', etc. Thus,
# the following could call 'preload_belongs_to_association',
# 'preload_has_many_association', etc.
@@ -128,7 +128,7 @@ module ActiveRecord
association_proxy.target.push(*[associated_record].flatten)
end
end
-
+
def add_preloaded_record_to_collection(parent_records, reflection_name, associated_record)
parent_records.each do |parent_record|
parent_record.send("set_#{reflection_name}_target", associated_record)
@@ -183,18 +183,19 @@ module ActiveRecord
conditions = "t0.#{reflection.primary_key_name} #{in_or_equals_for_ids(ids)}"
conditions << append_conditions(reflection, preload_options)
- associated_records = reflection.klass.find(:all, :conditions => [conditions, ids],
- :include => options[:include],
- :joins => "INNER JOIN #{connection.quote_table_name options[:join_table]} t0 ON #{reflection.klass.quoted_table_name}.#{reflection.klass.primary_key} = t0.#{reflection.association_foreign_key}",
- :select => "#{options[:select] || table_name+'.*'}, t0.#{reflection.primary_key_name} as the_parent_record_id",
- :order => options[:order])
-
+ associated_records = reflection.klass.with_exclusive_scope do
+ reflection.klass.find(:all, :conditions => [conditions, ids],
+ :include => options[:include],
+ :joins => "INNER JOIN #{connection.quote_table_name options[:join_table]} t0 ON #{reflection.klass.quoted_table_name}.#{reflection.klass.primary_key} = t0.#{reflection.association_foreign_key}",
+ :select => "#{options[:select] || table_name+'.*'}, t0.#{reflection.primary_key_name} as the_parent_record_id",
+ :order => options[:order])
+ end
set_association_collection_records(id_to_record_map, reflection.name, associated_records, 'the_parent_record_id')
end
def preload_has_one_association(records, reflection, preload_options={})
return if records.first.send("loaded_#{reflection.name}?")
- id_to_record_map, ids = construct_id_map(records)
+ id_to_record_map, ids = construct_id_map(records, reflection.options[:primary_key])
options = reflection.options
records.each {|record| record.send("set_#{reflection.name}_target", nil)}
if options[:through]
@@ -204,9 +205,18 @@ module ActiveRecord
unless through_records.empty?
source = reflection.source_reflection.name
through_records.first.class.preload_associations(through_records, source)
- through_records.each do |through_record|
- add_preloaded_record_to_collection(id_to_record_map[through_record[through_primary_key].to_s],
- reflection.name, through_record.send(source))
+ if through_reflection.macro == :belongs_to
+ rev_id_to_record_map, rev_ids = construct_id_map(records, through_primary_key)
+ rev_primary_key = through_reflection.klass.primary_key
+ through_records.each do |through_record|
+ add_preloaded_record_to_collection(rev_id_to_record_map[through_record[rev_primary_key].to_s],
+ reflection.name, through_record.send(source))
+ end
+ else
+ through_records.each do |through_record|
+ add_preloaded_record_to_collection(id_to_record_map[through_record[through_primary_key].to_s],
+ reflection.name, through_record.send(source))
+ end
end
end
else
@@ -219,7 +229,7 @@ module ActiveRecord
options = reflection.options
primary_key_name = reflection.through_reflection_primary_key_name
- id_to_record_map, ids = construct_id_map(records, primary_key_name)
+ id_to_record_map, ids = construct_id_map(records, primary_key_name || reflection.options[:primary_key])
records.each {|record| record.send(reflection.name).loaded}
if options[:through]
@@ -239,7 +249,7 @@ module ActiveRecord
reflection.primary_key_name)
end
end
-
+
def preload_through_records(records, reflection, through_association)
through_reflection = reflections[through_association]
through_primary_key = through_reflection.primary_key_name
@@ -307,6 +317,7 @@ module ActiveRecord
klasses_and_ids.each do |klass_and_id|
klass_name, id_map = *klass_and_id
+ next if id_map.empty?
klass = klass_name.constantize
table_name = klass.quoted_table_name
@@ -323,11 +334,13 @@ module ActiveRecord
end
conditions = "#{table_name}.#{connection.quote_column_name(primary_key)} #{in_or_equals_for_ids(ids)}"
conditions << append_conditions(reflection, preload_options)
- associated_records = klass.find(:all, :conditions => [conditions, ids],
+ associated_records = klass.with_exclusive_scope do
+ klass.find(:all, :conditions => [conditions, ids],
:include => options[:include],
:select => options[:select],
:joins => options[:joins],
:order => options[:order])
+ end
set_association_single_records(id_map, reflection.name, associated_records, primary_key)
end
end
@@ -345,13 +358,15 @@ module ActiveRecord
conditions << append_conditions(reflection, preload_options)
- reflection.klass.find(:all,
+ reflection.klass.with_exclusive_scope do
+ reflection.klass.find(:all,
:select => (preload_options[:select] || options[:select] || "#{table_name}.*"),
:include => preload_options[:include] || options[:include],
:conditions => [conditions, ids],
:joins => options[:joins],
:group => preload_options[:group] || options[:group],
:order => preload_options[:order] || options[:order])
+ end
end
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index 3fbbea43ed..86616abf52 100755
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -22,7 +22,7 @@ module ActiveRecord
through_reflection = reflection.through_reflection
source_reflection_names = reflection.source_reflection_names
source_associations = reflection.through_reflection.klass.reflect_on_all_associations.collect { |a| a.name.inspect }
- super("Could not find the source association(s) #{source_reflection_names.collect(&:inspect).to_sentence :connector => 'or'} in model #{through_reflection.klass}. Try 'has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}, :source => <name>'. Is it one of #{source_associations.to_sentence :connector => 'or'}?")
+ super("Could not find the source association(s) #{source_reflection_names.collect(&:inspect).to_sentence :two_words_connector => ' or ', :last_word_connector => ', or '} in model #{through_reflection.klass}. Try 'has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}, :source => <name>'. Is it one of #{source_associations.to_sentence :two_words_connector => ' or ', :last_word_connector => ', or '}?")
end
end
@@ -153,7 +153,7 @@ module ActiveRecord
# #others.destroy_all | X | X | X
# #others.find(*args) | X | X | X
# #others.find_first | X | |
- # #others.exist? | X | X | X
+ # #others.exists? | X | X | X
# #others.uniq | X | X | X
# #others.reset | X | X | X
#
@@ -652,7 +652,7 @@ module ActiveRecord
# Returns the number of associated objects.
# [collection.find(...)]
# Finds an associated object according to the same rules as ActiveRecord::Base.find.
- # [collection.exist?(...)]
+ # [collection.exists?(...)]
# Checks whether an associated object with the given conditions exists.
# Uses the same rules as ActiveRecord::Base.exists?.
# [collection.build(attributes = {}, ...)]
@@ -682,7 +682,7 @@ module ActiveRecord
# * <tt>Firm#clients.empty?</tt> (similar to <tt>firm.clients.size == 0</tt>)
# * <tt>Firm#clients.size</tt> (similar to <tt>Client.count "firm_id = #{id}"</tt>)
# * <tt>Firm#clients.find</tt> (similar to <tt>Client.find(id, :conditions => "firm_id = #{id}")</tt>)
- # * <tt>Firm#clients.exist?(:name => 'ACME')</tt> (similar to <tt>Client.exist?(:name => 'ACME', :firm_id => firm.id)</tt>)
+ # * <tt>Firm#clients.exists?(:name => 'ACME')</tt> (similar to <tt>Client.exists?(:name => 'ACME', :firm_id => firm.id)</tt>)
# * <tt>Firm#clients.build</tt> (similar to <tt>Client.new("firm_id" => id)</tt>)
# * <tt>Firm#clients.create</tt> (similar to <tt>c = Client.new("firm_id" => id); c.save; c</tt>)
# The declaration can also include an options hash to specialize the behavior of the association.
@@ -1107,7 +1107,7 @@ module ActiveRecord
# Finds an associated object responding to the +id+ and that
# meets the condition that it has to be associated with this object.
# Uses the same rules as ActiveRecord::Base.find.
- # [collection.exist?(...)]
+ # [collection.exists?(...)]
# Checks whether an associated object with the given conditions exists.
# Uses the same rules as ActiveRecord::Base.exists?.
# [collection.build(attributes = {})]
@@ -1133,7 +1133,7 @@ module ActiveRecord
# * <tt>Developer#projects.empty?</tt>
# * <tt>Developer#projects.size</tt>
# * <tt>Developer#projects.find(id)</tt>
- # * <tt>Developer#clients.exist?(...)</tt>
+ # * <tt>Developer#clients.exists?(...)</tt>
# * <tt>Developer#projects.build</tt> (similar to <tt>Project.new("project_id" => id)</tt>)
# * <tt>Developer#projects.create</tt> (similar to <tt>c = Project.new("project_id" => id); c.save; c</tt>)
# The declaration may include an options hash to specialize the behavior of the association.
@@ -1216,11 +1216,11 @@ module ActiveRecord
# callbacks will be executed after the association is wiped out.
old_method = "destroy_without_habtm_shim_for_#{reflection.name}"
class_eval <<-end_eval unless method_defined?(old_method)
- alias_method :#{old_method}, :destroy_without_callbacks
- def destroy_without_callbacks
- #{reflection.name}.clear
- #{old_method}
- end
+ alias_method :#{old_method}, :destroy_without_callbacks # alias_method :destroy_without_habtm_shim_for_posts, :destroy_without_callbacks
+ def destroy_without_callbacks # def destroy_without_callbacks
+ #{reflection.name}.clear # posts.clear
+ #{old_method} # destroy_without_habtm_shim_for_posts
+ end # end
end_eval
add_association_callbacks(reflection.name, options)
@@ -1453,7 +1453,7 @@ module ActiveRecord
dependent_conditions << sanitize_sql(reflection.options[:conditions]) if reflection.options[:conditions]
dependent_conditions << extra_conditions if extra_conditions
dependent_conditions = dependent_conditions.collect {|where| "(#{where})" }.join(" AND ")
-
+ dependent_conditions = dependent_conditions.gsub('@', '\@')
case reflection.options[:dependent]
when :destroy
method_name = "has_many_dependent_destroy_for_#{reflection.name}".to_sym
@@ -1463,22 +1463,22 @@ module ActiveRecord
before_destroy method_name
when :delete_all
module_eval %Q{
- before_destroy do |record|
- delete_all_has_many_dependencies(record,
- "#{reflection.name}",
- #{reflection.class_name},
- "#{dependent_conditions}")
- end
+ before_destroy do |record| # before_destroy do |record|
+ delete_all_has_many_dependencies(record, # delete_all_has_many_dependencies(record,
+ "#{reflection.name}", # "posts",
+ #{reflection.class_name}, # Post,
+ %@#{dependent_conditions}@) # %@...@) # this is a string literal like %(...)
+ end # end
}
when :nullify
module_eval %Q{
- before_destroy do |record|
- nullify_has_many_dependencies(record,
- "#{reflection.name}",
- #{reflection.class_name},
- "#{reflection.primary_key_name}",
- "#{dependent_conditions}")
- end
+ before_destroy do |record| # before_destroy do |record|
+ nullify_has_many_dependencies(record, # nullify_has_many_dependencies(record,
+ "#{reflection.name}", # "posts",
+ #{reflection.class_name}, # Post,
+ "#{reflection.primary_key_name}", # "user_id",
+ %@#{dependent_conditions}@) # %@...@) # this is a string literal like %(...)
+ end # end
}
else
raise ArgumentError, "The :dependent option expects either :destroy, :delete_all, or :nullify (#{reflection.options[:dependent].inspect})"
@@ -1731,6 +1731,11 @@ module ActiveRecord
return sanitize_sql(sql)
end
+ def tables_in_string(string)
+ return [] if string.blank?
+ string.scan(/([\.a-zA-Z_]+).?\./).flatten
+ end
+
def conditions_tables(options)
# look in both sets of conditions
conditions = [scope(:find, :conditions), options[:conditions]].inject([]) do |all, cond|
@@ -1741,37 +1746,55 @@ module ActiveRecord
else all << cond
end
end
- conditions.join(' ').scan(/([\.a-zA-Z_]+).?\./).flatten
+ tables_in_string(conditions.join(' '))
end
def order_tables(options)
order = [options[:order], scope(:find, :order) ].join(", ")
return [] unless order && order.is_a?(String)
- order.scan(/([\.a-zA-Z_]+).?\./).flatten
+ tables_in_string(order)
end
def selects_tables(options)
select = options[:select]
return [] unless select && select.is_a?(String)
- select.scan(/"?([\.a-zA-Z_]+)"?.?\./).flatten
+ tables_in_string(select)
+ end
+
+ def joined_tables(options)
+ scope = scope(:find)
+ joins = options[:joins]
+ merged_joins = scope && scope[:joins] && joins ? merge_joins(scope[:joins], joins) : (joins || scope && scope[:joins])
+ [table_name] + case merged_joins
+ when Symbol, Hash, Array
+ if array_of_strings?(merged_joins)
+ tables_in_string(merged_joins.join(' '))
+ else
+ join_dependency = ActiveRecord::Associations::ClassMethods::InnerJoinDependency.new(self, merged_joins, nil)
+ join_dependency.join_associations.collect {|join_association| [join_association.aliased_join_table_name, join_association.aliased_table_name]}.flatten.compact
+ end
+ else
+ tables_in_string(merged_joins)
+ end
end
# Checks if the conditions reference a table other than the current model table
- def include_eager_conditions?(options, tables = nil)
- ((tables || conditions_tables(options)) - [table_name]).any?
+ def include_eager_conditions?(options, tables = nil, joined_tables = nil)
+ ((tables || conditions_tables(options)) - (joined_tables || joined_tables(options))).any?
end
# Checks if the query order references a table other than the current model's table.
- def include_eager_order?(options, tables = nil)
- ((tables || order_tables(options)) - [table_name]).any?
+ def include_eager_order?(options, tables = nil, joined_tables = nil)
+ ((tables || order_tables(options)) - (joined_tables || joined_tables(options))).any?
end
- def include_eager_select?(options)
- (selects_tables(options) - [table_name]).any?
+ def include_eager_select?(options, joined_tables = nil)
+ (selects_tables(options) - (joined_tables || joined_tables(options))).any?
end
def references_eager_loaded_tables?(options)
- include_eager_order?(options) || include_eager_conditions?(options) || include_eager_select?(options)
+ joined_tables = joined_tables(options)
+ include_eager_order?(options, nil, joined_tables) || include_eager_conditions?(options, nil, joined_tables) || include_eager_select?(options, joined_tables)
end
def using_limitable_reflections?(reflections)
@@ -2148,7 +2171,7 @@ module ActiveRecord
aliased_table_name,
foreign_key,
parent.aliased_table_name,
- parent.primary_key
+ reflection.options[:primary_key] || parent.primary_key
]
end
when :belongs_to
@@ -2175,7 +2198,7 @@ module ActiveRecord
protected
def aliased_table_name_for(name, suffix = nil)
- if !parent.table_joins.blank? && parent.table_joins.to_s.downcase =~ %r{join(\s+\w+)?\s+#{name.downcase}\son}
+ if !parent.table_joins.blank? && parent.table_joins.to_s.downcase =~ %r{join(\s+\w+)?\s+#{active_record.connection.quote_table_name name.downcase}\son}
@join_dependency.table_aliases[name] += 1
end
diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb
index 0ff91fbdf8..0fefec1216 100644
--- a/activerecord/lib/active_record/associations/association_collection.rb
+++ b/activerecord/lib/active_record/associations/association_collection.rb
@@ -83,7 +83,11 @@ module ActiveRecord
def to_ary
load_target
- @target.to_ary
+ if @target.is_a?(Array)
+ @target.to_ary
+ else
+ Array(@target)
+ end
end
def reset
diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb
index 59f1d3b867..676c4ace61 100644
--- a/activerecord/lib/active_record/associations/association_proxy.rb
+++ b/activerecord/lib/active_record/associations/association_proxy.rb
@@ -180,7 +180,10 @@ module ActiveRecord
record["#{@reflection.options[:as]}_id"] = @owner.id unless @owner.new_record?
record["#{@reflection.options[:as]}_type"] = @owner.class.base_class.name.to_s
else
- record[@reflection.primary_key_name] = @owner.id unless @owner.new_record?
+ unless @owner.new_record?
+ primary_key = @reflection.options[:primary_key] || :id
+ record[@reflection.primary_key_name] = @owner.send(primary_key)
+ 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 a0bb3a45b0..2eeeb28d1f 100644
--- a/activerecord/lib/active_record/associations/has_many_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_through_association.rb
@@ -160,9 +160,9 @@ module ActiveRecord
end
"INNER JOIN %s ON %s.%s = %s.%s %s #{@reflection.options[:joins]} #{custom_joins}" % [
- @reflection.through_reflection.table_name,
- @reflection.table_name, reflection_primary_key,
- @reflection.through_reflection.table_name, source_primary_key,
+ @reflection.through_reflection.quoted_table_name,
+ @reflection.quoted_table_name, reflection_primary_key,
+ @reflection.through_reflection.quoted_table_name, source_primary_key,
polymorphic_join
]
end
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index 5d614442c3..cca012ed55 100755
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -811,8 +811,7 @@ module ActiveRecord #:nodoc:
#
# ==== Parameters
#
- # * +updates+ - A string of column and value pairs that will be set on any records that match conditions.
- # What goes into the SET clause.
+ # * +updates+ - A string of column and value pairs that will be set on any records that match conditions. This creates the SET clause of the generated SQL.
# * +conditions+ - An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. See conditions in the intro for more info.
# * +options+ - Additional options are <tt>:limit</tt> and <tt>:order</tt>, see the examples for usage.
#
@@ -1417,8 +1416,8 @@ module ActiveRecord #:nodoc:
def benchmark(title, log_level = Logger::DEBUG, use_silence = true)
if logger && logger.level <= log_level
result = nil
- seconds = Benchmark.realtime { result = use_silence ? silence { yield } : yield }
- logger.add(log_level, "#{title} (#{'%.1f' % (seconds * 1000)}ms)")
+ ms = Benchmark.ms { result = use_silence ? silence { yield } : yield }
+ logger.add(log_level, '%s (%.1fms)' % [title, ms])
result
else
yield
@@ -1457,7 +1456,10 @@ module ActiveRecord #:nodoc:
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
@@ -1495,11 +1497,16 @@ module ActiveRecord #:nodoc:
end
if scoped?(:find, :order)
- scoped_order = reverse_sql_order(scope(:find, :order))
- scoped_methods.select { |s| s[:find].update(:order => scoped_order) }
+ scope = scope(:find)
+ original_scoped_order = scope[:order]
+ scope[:order] = reverse_sql_order(original_scoped_order)
end
- find_initial(options.merge({ :order => order }))
+ begin
+ find_initial(options.merge({ :order => order }))
+ ensure
+ scope[:order] = original_scoped_order if original_scoped_order
+ end
end
def reverse_sql_order(order_query)
@@ -1805,7 +1812,11 @@ module ActiveRecord #:nodoc:
# This also enables you to initialize a record if it is not found, such as find_or_initialize_by_amount(amount)
# or find_or_create_by_user_and_password(user, password).
#
- # Each dynamic finder or initializer/creator is also defined in the class after it is first invoked, so that future
+ # Also enables dynamic scopes like scoped_by_user_name(user_name) and scoped_by_user_name_and_password(user_name, password) that
+ # are turned into scoped(:conditions => ["user_name = ?", user_name]) and scoped(:conditions => ["user_name = ? AND password = ?", user_name, password])
+ # respectively.
+ #
+ # Each dynamic finder, scope or initializer/creator 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)
@@ -1814,10 +1825,31 @@ module ActiveRecord #:nodoc:
if match.finder?
finder = match.finder
bang = match.bang?
+ # def self.find_by_login_and_activated(*args)
+ # options = args.extract_options!
+ # attributes = construct_attributes_from_arguments(
+ # [:login,:activated],
+ # args
+ # )
+ # finder_options = { :conditions => attributes }
+ # validate_find_options(options)
+ # set_readonly_option!(options)
+ #
+ # if options[:conditions]
+ # with_scope(:find => finder_options) do
+ # find(:first, options)
+ # end
+ # else
+ # find(:first, options.merge(finder_options))
+ # end
+ # end
self.class_eval %{
def self.#{method_id}(*args)
options = args.extract_options!
- attributes = construct_attributes_from_arguments([:#{attribute_names.join(',:')}], args)
+ attributes = construct_attributes_from_arguments(
+ [:#{attribute_names.join(',:')}],
+ args
+ )
finder_options = { :conditions => attributes }
validate_find_options(options)
set_readonly_option!(options)
@@ -1829,12 +1861,37 @@ module ActiveRecord #:nodoc:
else
find(:#{finder}, options.merge(finder_options))
end
- #{'result || raise(RecordNotFound)' if bang}
+ #{'result || raise(RecordNotFound, "Couldn\'t find #{name} with #{attributes.to_a.collect {|pair| "#{pair.first} = #{pair.second}"}.join(\', \')}")' if bang}
end
}, __FILE__, __LINE__
send(method_id, *arguments)
elsif match.instantiator?
instantiator = match.instantiator
+ # def self.find_or_create_by_user_id(*args)
+ # guard_protected_attributes = false
+ #
+ # if args[0].is_a?(Hash)
+ # guard_protected_attributes = true
+ # attributes = args[0].with_indifferent_access
+ # find_attributes = attributes.slice(*[:user_id])
+ # else
+ # find_attributes = attributes = construct_attributes_from_arguments([:user_id], args)
+ # end
+ #
+ # options = { :conditions => find_attributes }
+ # set_readonly_option!(options)
+ #
+ # record = find(:first, options)
+ #
+ # if record.nil?
+ # record = self.new { |r| r.send(:attributes=, attributes, guard_protected_attributes) }
+ # yield(record) if block_given?
+ # record.save
+ # record
+ # else
+ # record
+ # end
+ # end
self.class_eval %{
def self.#{method_id}(*args)
guard_protected_attributes = false
@@ -1864,6 +1921,22 @@ module ActiveRecord #:nodoc:
}, __FILE__, __LINE__
send(method_id, *arguments, &block)
end
+ elsif match = DynamicScopeMatch.match(method_id)
+ attribute_names = match.attribute_names
+ super unless all_attributes_exists?(attribute_names)
+ if match.scope?
+ self.class_eval %{
+ def self.#{method_id}(*args) # def self.scoped_by_user_name_and_password(*args)
+ options = args.extract_options! # options = args.extract_options!
+ attributes = construct_attributes_from_arguments( # attributes = construct_attributes_from_arguments(
+ [:#{attribute_names.join(',:')}], args # [:user_name, :password], args
+ ) # )
+ #
+ scoped(:conditions => attributes) # scoped(:conditions => attributes)
+ end # end
+ }, __FILE__, __LINE__
+ send(method_id, *arguments)
+ end
else
super
end
@@ -2052,10 +2125,10 @@ module ActiveRecord #:nodoc:
end
# Sets the default options for the model. The format of the
- # <tt>method_scoping</tt> argument is the same as in with_scope.
+ # <tt>options</tt> argument is the same as in find.
#
# class Person < ActiveRecord::Base
- # default_scope :find => { :order => 'last_name, first_name' }
+ # default_scope :order => 'last_name, first_name'
# end
def default_scope(options = {})
self.default_scoping << { :find => options, :create => (options.is_a?(Hash) && options.has_key?(:conditions)) ? options[:conditions] : {} }
@@ -2402,9 +2475,9 @@ module ActiveRecord #:nodoc:
write_attribute(self.class.primary_key, value)
end
- # Returns true if this object hasn't been saved yet -- that is, a record for the object doesn't exist yet.
+ # Returns true if this object hasn't been saved yet -- that is, a record for the object doesn't exist yet; otherwise, returns false.
def new_record?
- defined?(@new_record) && @new_record
+ @new_record || false
end
# :call-seq:
@@ -3011,7 +3084,7 @@ module ActiveRecord #:nodoc:
end
Base.class_eval do
- extend QueryCache
+ extend QueryCache::ClassMethods
include Validations
include Locking::Optimistic, Locking::Pessimistic
include AttributeMethods
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb
index ccb79f547a..bbc290f721 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb
@@ -123,6 +123,7 @@ module ActiveRecord
connection_handler.retrieve_connection(self)
end
+ # Returns true if +ActiveRecord+ is connected.
def connected?
connection_handler.connected?(self)
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
index 950bd72101..00c71090f3 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
@@ -14,12 +14,12 @@ module ActiveRecord
def dirties_query_cache(base, *method_names)
method_names.each do |method_name|
base.class_eval <<-end_code, __FILE__, __LINE__
- def #{method_name}_with_query_dirty(*args)
- clear_query_cache if @query_cache_enabled
- #{method_name}_without_query_dirty(*args)
- end
-
- alias_method_chain :#{method_name}, :query_dirty
+ def #{method_name}_with_query_dirty(*args) # def update_with_query_dirty(*args)
+ clear_query_cache if @query_cache_enabled # clear_query_cache if @query_cache_enabled
+ #{method_name}_without_query_dirty(*args) # update_without_query_dirty(*args)
+ end # end
+ #
+ alias_method_chain :#{method_name}, :query_dirty # alias_method_chain :update, :query_dirty
end_code
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
index 58992f91da..273f823e7f 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
@@ -32,10 +32,12 @@ module ActiveRecord
@primary = nil
end
+ # Returns +true+ if the column is either of type string or text.
def text?
type == :string || type == :text
end
+ # Returns +true+ if the column is either of type integer, float or decimal.
def number?
type == :integer || type == :float || type == :decimal
end
@@ -295,7 +297,7 @@ module ActiveRecord
# puts t.class # => "ActiveRecord::ConnectionAdapters::TableDefinition"
# end
# end
- #
+ #
# def self.down
# ...
# end
@@ -474,12 +476,12 @@ module ActiveRecord
%w( string text integer float decimal datetime timestamp time date binary boolean ).each do |column_type|
class_eval <<-EOV
- def #{column_type}(*args)
- options = args.extract_options!
- column_names = args
-
- column_names.each { |name| column(name, '#{column_type}', options) }
- end
+ 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
EOV
end
@@ -674,24 +676,24 @@ module ActiveRecord
# t.string(:goat, :sheep)
%w( string text integer float decimal datetime timestamp time date binary boolean ).each do |column_type|
class_eval <<-EOV
- def #{column_type}(*args)
- options = args.extract_options!
- column_names = args
-
- column_names.each do |name|
- column = ColumnDefinition.new(@base, name, '#{column_type}')
- if options[:limit]
- column.limit = options[:limit]
- elsif native['#{column_type}'.to_sym].is_a?(Hash)
- column.limit = native['#{column_type}'.to_sym][:limit]
- end
- column.precision = options[:precision]
- column.scale = options[:scale]
- column.default = options[:default]
- column.null = options[:null]
- @base.add_column(@table_name, name, column.sql_type, options)
- end
- end
+ def #{column_type}(*args) # def string(*args)
+ options = args.extract_options! # options = args.extract_options!
+ column_names = args # column_names = args
+ #
+ column_names.each do |name| # column_names.each do |name|
+ column = ColumnDefinition.new(@base, name, '#{column_type}') # column = ColumnDefinition.new(@base, name, 'string')
+ 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]
+ end # end
+ column.precision = options[:precision] # column.precision = options[:precision]
+ column.scale = options[:scale] # column.scale = options[:scale]
+ column.default = options[:default] # column.default = options[:default]
+ column.null = options[:null] # column.null = options[:null]
+ @base.add_column(@table_name, name, column.sql_type, options) # @base.add_column(@table_name, name, column.sql_type, options)
+ end # end
+ end # end
EOV
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
index b26185580c..5137b0f78c 100755
--- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -184,9 +184,9 @@ module ActiveRecord
# for more information about the effect of this option.
attr_accessor :transactional_fixtures
- def log_info(sql, name, seconds)
+ def log_info(sql, name, ms)
if @logger && @logger.debug?
- name = "#{name.nil? ? "SQL" : name} (#{sprintf("%.1f", seconds * 1000)}ms)"
+ name = '%s (%.1fms)' % [name || 'SQL', ms]
@logger.debug(format_log_entry(name, sql.squeeze(' ')))
end
end
@@ -195,9 +195,9 @@ module ActiveRecord
def log(sql, name)
if block_given?
result = nil
- seconds = Benchmark.realtime { result = yield }
- @runtime += seconds
- log_info(sql, name, seconds)
+ ms = Benchmark.ms { result = yield }
+ @runtime += ms
+ log_info(sql, name, ms)
result
else
log_info(sql, name, 0)
diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
index 3fbbb46f73..b2345fd571 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
@@ -13,23 +13,25 @@ module MysqlCompat #:nodoc:
# C driver >= 2.7 returns null values in each_hash
if Mysql.const_defined?(:VERSION) && (Mysql::VERSION.is_a?(String) || Mysql::VERSION >= 20700)
target.class_eval <<-'end_eval'
- def all_hashes
- rows = []
- each_hash { |row| rows << row }
- rows
- end
+ def all_hashes # def all_hashes
+ rows = [] # rows = []
+ each_hash { |row| rows << row } # each_hash { |row| rows << row }
+ rows # rows
+ end # end
end_eval
# adapters before 2.7 don't have a version constant
# and don't return null values in each_hash
else
target.class_eval <<-'end_eval'
- def all_hashes
- rows = []
- all_fields = fetch_fields.inject({}) { |fields, f| fields[f.name] = nil; fields }
- each_hash { |row| rows << all_fields.dup.update(row) }
- rows
- end
+ def all_hashes # def all_hashes
+ rows = [] # rows = []
+ all_fields = fetch_fields.inject({}) { |fields, f| # all_fields = fetch_fields.inject({}) { |fields, f|
+ fields[f.name] = nil; fields # fields[f.name] = nil; fields
+ } # }
+ each_hash { |row| rows << all_fields.dup.update(row) } # each_hash { |row| rows << all_fields.dup.update(row) }
+ rows # rows
+ end # end
end_eval
end
@@ -312,6 +314,7 @@ module ActiveRecord
rows
end
+ # Executes a SQL query and returns a MySQL::Result object. Note that you have to free the Result object after you're done using it.
def execute(sql, name = nil) #:nodoc:
log(sql, name) { @connection.query(sql) }
rescue ActiveRecord::StatementInvalid => exception
@@ -429,7 +432,9 @@ module ActiveRecord
def tables(name = nil) #:nodoc:
tables = []
- execute("SHOW TABLES", name).each { |field| tables << field[0] }
+ result = execute("SHOW TABLES", name)
+ result.each { |field| tables << field[0] }
+ result.free
tables
end
@@ -440,7 +445,8 @@ module ActiveRecord
def indexes(table_name, name = nil)#:nodoc:
indexes = []
current_index = nil
- execute("SHOW KEYS FROM #{quote_table_name(table_name)}", name).each do |row|
+ result = execute("SHOW KEYS FROM #{quote_table_name(table_name)}", name)
+ result.each do |row|
if current_index != row[2]
next if row[2] == "PRIMARY" # skip the primary key
current_index = row[2]
@@ -449,13 +455,16 @@ module ActiveRecord
indexes.last.columns << row[4]
end
+ result.free
indexes
end
def columns(table_name, name = nil)#:nodoc:
sql = "SHOW FIELDS FROM #{quote_table_name(table_name)}"
columns = []
- execute(sql, name).each { |field| columns << MysqlColumn.new(field[0], field[4], field[1], field[2] == "YES") }
+ result = execute(sql, name)
+ result.each { |field| columns << MysqlColumn.new(field[0], field[4], field[1], field[2] == "YES") }
+ result.free
columns
end
@@ -536,9 +545,11 @@ module ActiveRecord
# Returns a table's primary key and belonging sequence.
def pk_and_sequence_for(table) #:nodoc:
keys = []
- execute("describe #{quote_table_name(table)}").each_hash do |h|
+ result = execute("describe #{quote_table_name(table)}")
+ result.each_hash do |h|
keys << h["Field"]if h["Key"] == "PRI"
end
+ result.free
keys.length == 1 ? [keys.first, nil] : nil
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
index c4ef2be82e..5a8d99924d 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -937,13 +937,13 @@ module ActiveRecord
# should know about this but can't detect it there, so deal with it here.
money_precision = (postgresql_version >= 80300) ? 19 : 10
PostgreSQLColumn.module_eval(<<-end_eval)
- def extract_precision(sql_type)
- if sql_type =~ /^money$/
- #{money_precision}
- else
- super
- end
- end
+ def extract_precision(sql_type) # def extract_precision(sql_type)
+ if sql_type =~ /^money$/ # if sql_type =~ /^money$/
+ #{money_precision} # 19
+ else # else
+ super # super
+ end # end
+ end # end
end_eval
configure_connection
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
index 84f8c0284e..9387cf8827 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
@@ -402,6 +402,10 @@ module ActiveRecord
end
def add_column(table_name, column_name, type, options = {}) #:nodoc:
+ if @connection.respond_to?(:transaction_active?) && @connection.transaction_active?
+ raise StatementInvalid, 'Cannot add columns to a SQLite database while inside a transaction'
+ end
+
alter_table(table_name) do |definition|
definition.column(column_name, type, options)
end
diff --git a/activerecord/lib/active_record/dirty.rb b/activerecord/lib/active_record/dirty.rb
index a1760875ba..4c899f58e5 100644
--- a/activerecord/lib/active_record/dirty.rb
+++ b/activerecord/lib/active_record/dirty.rb
@@ -174,7 +174,7 @@ module ActiveRecord
alias_attribute_without_dirty(new_name, old_name)
DIRTY_SUFFIXES.each do |suffix|
module_eval <<-STR, __FILE__, __LINE__+1
- def #{new_name}#{suffix}; self.#{old_name}#{suffix}; end
+ def #{new_name}#{suffix}; self.#{old_name}#{suffix}; end # def subject_changed?; self.title_changed?; end
STR
end
end
diff --git a/activerecord/lib/active_record/dynamic_scope_match.rb b/activerecord/lib/active_record/dynamic_scope_match.rb
new file mode 100644
index 0000000000..f796ba669a
--- /dev/null
+++ b/activerecord/lib/active_record/dynamic_scope_match.rb
@@ -0,0 +1,25 @@
+module ActiveRecord
+ class DynamicScopeMatch
+ def self.match(method)
+ ds_match = self.new(method)
+ ds_match.scope ? ds_match : nil
+ end
+
+ def initialize(method)
+ @scope = true
+ case method.to_s
+ when /^scoped_by_([_a-zA-Z]\w*)$/
+ names = $1
+ else
+ @scope = nil
+ end
+ @attribute_names = names && names.split('_and_')
+ end
+
+ attr_reader :scope, :attribute_names
+
+ def scope?
+ !@scope.nil?
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/query_cache.rb b/activerecord/lib/active_record/query_cache.rb
index a8af89fcb9..eb92bc2545 100644
--- a/activerecord/lib/active_record/query_cache.rb
+++ b/activerecord/lib/active_record/query_cache.rb
@@ -1,20 +1,32 @@
module ActiveRecord
- module QueryCache
- # Enable the query cache within the block if Active Record is configured.
- def cache(&block)
- if ActiveRecord::Base.configurations.blank?
- yield
- else
- connection.cache(&block)
+ class QueryCache
+ module ClassMethods
+ # Enable the query cache within the block if Active Record is configured.
+ def cache(&block)
+ if ActiveRecord::Base.configurations.blank?
+ yield
+ else
+ connection.cache(&block)
+ end
end
+
+ # Disable the query cache within the block if Active Record is configured.
+ def uncached(&block)
+ if ActiveRecord::Base.configurations.blank?
+ yield
+ else
+ connection.uncached(&block)
+ end
+ end
+ end
+
+ def initialize(app)
+ @app = app
end
- # Disable the query cache within the block if Active Record is configured.
- def uncached(&block)
- if ActiveRecord::Base.configurations.blank?
- yield
- else
- connection.uncached(&block)
+ def call(env)
+ ActiveRecord::Base.cache do
+ @app.call(env)
end
end
end
diff --git a/activerecord/lib/active_record/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb
index d171b742f5..4749823b94 100644
--- a/activerecord/lib/active_record/serializers/xml_serializer.rb
+++ b/activerecord/lib/active_record/serializers/xml_serializer.rb
@@ -23,11 +23,12 @@ module ActiveRecord #:nodoc:
# </topic>
#
# This behavior can be controlled with <tt>:only</tt>, <tt>:except</tt>,
- # <tt>:skip_instruct</tt>, <tt>:skip_types</tt> and <tt>:dasherize</tt>.
+ # <tt>:skip_instruct</tt>, <tt>:skip_types</tt>, <tt>:dasherize</tt> and <tt>:camelize</tt> .
# The <tt>:only</tt> and <tt>:except</tt> options are the same as for the
# +attributes+ method. The default is to dasherize all column names, but you
- # can disable this setting <tt>:dasherize</tt> to +false+. To not have the
- # column type included in the XML output set <tt>:skip_types</tt> to +true+.
+ # can disable this setting <tt>:dasherize</tt> to +false+. Setting <tt>:camelize</tt>
+ # to +true+ will camelize all column names - this also overrides <tt>:dasherize</tt>.
+ # To not have the column type included in the XML output set <tt>:skip_types</tt> to +true+.
#
# For instance:
#
@@ -178,13 +179,22 @@ module ActiveRecord #:nodoc:
def root
root = (options[:root] || @record.class.to_s.underscore).to_s
- dasherize? ? root.dasherize : root
+ reformat_name(root)
end
def dasherize?
!options.has_key?(:dasherize) || options[:dasherize]
end
+ def camelize?
+ options.has_key?(:camelize) && options[:camelize]
+ end
+
+ def reformat_name(name)
+ name = name.camelize if camelize?
+ dasherize? ? name.dasherize : name
+ end
+
def serializable_attributes
serializable_attribute_names.collect { |name| Attribute.new(name, @record) }
end
@@ -212,7 +222,7 @@ module ActiveRecord #:nodoc:
def add_tag(attribute)
builder.tag!(
- dasherize? ? attribute.name.dasherize : attribute.name,
+ reformat_name(attribute.name),
attribute.value.to_s,
attribute.decorations(!options[:skip_types])
)
@@ -220,8 +230,7 @@ module ActiveRecord #:nodoc:
def add_associations(association, records, opts)
if records.is_a?(Enumerable)
- tag = association.to_s
- tag = tag.dasherize if dasherize?
+ tag = reformat_name(association.to_s)
if records.empty?
builder.tag!(tag, :type => :array)
else
diff --git a/activerecord/lib/active_record/session_store.rb b/activerecord/lib/active_record/session_store.rb
new file mode 100644
index 0000000000..bd198c03b2
--- /dev/null
+++ b/activerecord/lib/active_record/session_store.rb
@@ -0,0 +1,319 @@
+module ActiveRecord
+ # A session store backed by an Active Record class. A default class is
+ # provided, but any object duck-typing to an Active Record Session class
+ # with text +session_id+ and +data+ attributes is sufficient.
+ #
+ # The default assumes a +sessions+ tables with columns:
+ # +id+ (numeric primary key),
+ # +session_id+ (text, or longtext if your session data exceeds 65K), and
+ # +data+ (text or longtext; careful if your session data exceeds 65KB).
+ # The +session_id+ column should always be indexed for speedy lookups.
+ # Session data is marshaled to the +data+ column in Base64 format.
+ # If the data you write is larger than the column's size limit,
+ # ActionController::SessionOverflowError will be raised.
+ #
+ # You may configure the table name, primary key, and data column.
+ # For example, at the end of <tt>config/environment.rb</tt>:
+ # ActiveRecord::SessionStore::Session.table_name = 'legacy_session_table'
+ # ActiveRecord::SessionStore::Session.primary_key = 'session_id'
+ # ActiveRecord::SessionStore::Session.data_column_name = 'legacy_session_data'
+ # Note that setting the primary key to the +session_id+ frees you from
+ # having a separate +id+ column if you don't want it. However, you must
+ # set <tt>session.model.id = session.session_id</tt> by hand! A before filter
+ # on ApplicationController is a good place.
+ #
+ # Since the default class is a simple Active Record, you get timestamps
+ # for free if you add +created_at+ and +updated_at+ datetime columns to
+ # the +sessions+ table, making periodic session expiration a snap.
+ #
+ # You may provide your own session class implementation, whether a
+ # feature-packed Active Record or a bare-metal high-performance SQL
+ # store, by setting
+ # ActiveRecord::SessionStore.session_class = MySessionClass
+ # You must implement these methods:
+ # self.find_by_session_id(session_id)
+ # initialize(hash_of_session_id_and_data)
+ # attr_reader :session_id
+ # attr_accessor :data
+ # save
+ # destroy
+ #
+ # The example SqlBypass class is a generic SQL session store. You may
+ # use it as a basis for high-performance database-specific stores.
+ class SessionStore < ActionController::Session::AbstractStore
+ # The default Active Record class.
+ class Session < ActiveRecord::Base
+ ##
+ # :singleton-method:
+ # Customizable data column name. Defaults to 'data'.
+ cattr_accessor :data_column_name
+ self.data_column_name = 'data'
+
+ before_save :marshal_data!
+ before_save :raise_on_session_data_overflow!
+
+ class << self
+ # Don't try to reload ARStore::Session in dev mode.
+ def reloadable? #:nodoc:
+ false
+ end
+
+ def data_column_size_limit
+ @data_column_size_limit ||= columns_hash[@@data_column_name].limit
+ end
+
+ # Hook to set up sessid compatibility.
+ def find_by_session_id(session_id)
+ setup_sessid_compatibility!
+ find_by_session_id(session_id)
+ end
+
+ def marshal(data)
+ ActiveSupport::Base64.encode64(Marshal.dump(data)) if data
+ end
+
+ def unmarshal(data)
+ Marshal.load(ActiveSupport::Base64.decode64(data)) if data
+ end
+
+ def create_table!
+ connection.execute <<-end_sql
+ CREATE TABLE #{table_name} (
+ id INTEGER PRIMARY KEY,
+ #{connection.quote_column_name('session_id')} TEXT UNIQUE,
+ #{connection.quote_column_name(@@data_column_name)} TEXT(255)
+ )
+ end_sql
+ end
+
+ def drop_table!
+ connection.execute "DROP TABLE #{table_name}"
+ end
+
+ private
+ # Compatibility with tables using sessid instead of session_id.
+ def setup_sessid_compatibility!
+ # Reset column info since it may be stale.
+ reset_column_information
+ if columns_hash['sessid']
+ def self.find_by_session_id(*args)
+ find_by_sessid(*args)
+ end
+
+ define_method(:session_id) { sessid }
+ define_method(:session_id=) { |session_id| self.sessid = session_id }
+ else
+ def self.find_by_session_id(session_id)
+ find :first, :conditions => ["session_id #{attribute_condition(session_id)}", session_id]
+ end
+ end
+ end
+ end
+
+ # Lazy-unmarshal session state.
+ def data
+ @data ||= self.class.unmarshal(read_attribute(@@data_column_name)) || {}
+ end
+
+ attr_writer :data
+
+ # Has the session been loaded yet?
+ def loaded?
+ !!@data
+ end
+
+ private
+ def marshal_data!
+ return false if !loaded?
+ write_attribute(@@data_column_name, self.class.marshal(self.data))
+ end
+
+ # Ensures that the data about to be stored in the database is not
+ # larger than the data storage column. Raises
+ # ActionController::SessionOverflowError.
+ def raise_on_session_data_overflow!
+ return false if !loaded?
+ limit = self.class.data_column_size_limit
+ if loaded? and limit and read_attribute(@@data_column_name).size > limit
+ raise ActionController::SessionOverflowError
+ end
+ end
+ end
+
+ # A barebones session store which duck-types with the default session
+ # store but bypasses Active Record and issues SQL directly. This is
+ # an example session model class meant as a basis for your own classes.
+ #
+ # The database connection, table name, and session id and data columns
+ # are configurable class attributes. Marshaling and unmarshaling
+ # are implemented as class methods that you may override. By default,
+ # marshaling data is
+ #
+ # ActiveSupport::Base64.encode64(Marshal.dump(data))
+ #
+ # and unmarshaling data is
+ #
+ # Marshal.load(ActiveSupport::Base64.decode64(data))
+ #
+ # This marshaling behavior is intended to store the widest range of
+ # binary session data in a +text+ column. For higher performance,
+ # store in a +blob+ column instead and forgo the Base64 encoding.
+ class SqlBypass
+ ##
+ # :singleton-method:
+ # Use the ActiveRecord::Base.connection by default.
+ cattr_accessor :connection
+
+ ##
+ # :singleton-method:
+ # The table name defaults to 'sessions'.
+ cattr_accessor :table_name
+ @@table_name = 'sessions'
+
+ ##
+ # :singleton-method:
+ # The session id field defaults to 'session_id'.
+ cattr_accessor :session_id_column
+ @@session_id_column = 'session_id'
+
+ ##
+ # :singleton-method:
+ # The data field defaults to 'data'.
+ cattr_accessor :data_column
+ @@data_column = 'data'
+
+ class << self
+ def connection
+ @@connection ||= ActiveRecord::Base.connection
+ end
+
+ # Look up a session by id and unmarshal its data if found.
+ def find_by_session_id(session_id)
+ if record = @@connection.select_one("SELECT * FROM #{@@table_name} WHERE #{@@session_id_column}=#{@@connection.quote(session_id)}")
+ new(:session_id => session_id, :marshaled_data => record['data'])
+ end
+ end
+
+ def marshal(data)
+ ActiveSupport::Base64.encode64(Marshal.dump(data)) if data
+ end
+
+ def unmarshal(data)
+ Marshal.load(ActiveSupport::Base64.decode64(data)) if data
+ end
+
+ def create_table!
+ @@connection.execute <<-end_sql
+ CREATE TABLE #{table_name} (
+ id INTEGER PRIMARY KEY,
+ #{@@connection.quote_column_name(session_id_column)} TEXT UNIQUE,
+ #{@@connection.quote_column_name(data_column)} TEXT
+ )
+ end_sql
+ end
+
+ def drop_table!
+ @@connection.execute "DROP TABLE #{table_name}"
+ end
+ end
+
+ attr_reader :session_id
+ attr_writer :data
+
+ # Look for normal and marshaled data, self.find_by_session_id's way of
+ # telling us to postpone unmarshaling until the data is requested.
+ # We need to handle a normal data attribute in case of a new record.
+ def initialize(attributes)
+ @session_id, @data, @marshaled_data = attributes[:session_id], attributes[:data], attributes[:marshaled_data]
+ @new_record = @marshaled_data.nil?
+ end
+
+ def new_record?
+ @new_record
+ end
+
+ # Lazy-unmarshal session state.
+ def data
+ unless @data
+ if @marshaled_data
+ @data, @marshaled_data = self.class.unmarshal(@marshaled_data) || {}, nil
+ else
+ @data = {}
+ end
+ end
+ @data
+ end
+
+ def loaded?
+ !!@data
+ end
+
+ def save
+ return false if !loaded?
+ marshaled_data = self.class.marshal(data)
+
+ if @new_record
+ @new_record = false
+ @@connection.update <<-end_sql, 'Create session'
+ INSERT INTO #{@@table_name} (
+ #{@@connection.quote_column_name(@@session_id_column)},
+ #{@@connection.quote_column_name(@@data_column)} )
+ VALUES (
+ #{@@connection.quote(session_id)},
+ #{@@connection.quote(marshaled_data)} )
+ end_sql
+ else
+ @@connection.update <<-end_sql, 'Update session'
+ UPDATE #{@@table_name}
+ SET #{@@connection.quote_column_name(@@data_column)}=#{@@connection.quote(marshaled_data)}
+ WHERE #{@@connection.quote_column_name(@@session_id_column)}=#{@@connection.quote(session_id)}
+ end_sql
+ end
+ end
+
+ def destroy
+ unless @new_record
+ @@connection.delete <<-end_sql, 'Destroy session'
+ DELETE FROM #{@@table_name}
+ WHERE #{@@connection.quote_column_name(@@session_id_column)}=#{@@connection.quote(session_id)}
+ end_sql
+ end
+ end
+ end
+
+ # The class used for session storage. Defaults to
+ # ActiveRecord::SessionStore::Session
+ cattr_accessor :session_class
+ self.session_class = Session
+
+ SESSION_RECORD_KEY = 'rack.session.record'.freeze
+
+ private
+ def get_session(env, sid)
+ Base.silence do
+ sid ||= generate_sid
+ session = @@session_class.find_by_session_id(sid)
+ session ||= @@session_class.new(:session_id => sid, :data => {})
+ env[SESSION_RECORD_KEY] = session
+ [sid, session.data]
+ end
+ end
+
+ def set_session(env, sid, session_data)
+ Base.silence do
+ record = env[SESSION_RECORD_KEY]
+ record.data = session_data
+ return false unless record.save
+
+ session_data = record.data
+ if session_data && session_data.respond_to?(:each_value)
+ session_data.each_value do |obj|
+ obj.clear_association_cache if obj.respond_to?(:clear_association_cache)
+ end
+ end
+ end
+
+ return true
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb
index a9e0efa6fe..8dbe80a01a 100644
--- a/activerecord/lib/active_record/timestamp.rb
+++ b/activerecord/lib/active_record/timestamp.rb
@@ -23,8 +23,8 @@ module ActiveRecord
write_attribute('created_at', t) if respond_to?(:created_at) && created_at.nil?
write_attribute('created_on', t) if respond_to?(:created_on) && created_on.nil?
- write_attribute('updated_at', t) if respond_to?(:updated_at)
- write_attribute('updated_on', t) if respond_to?(:updated_on)
+ write_attribute('updated_at', t) if respond_to?(:updated_at) && updated_at.nil?
+ write_attribute('updated_on', t) if respond_to?(:updated_on) && updated_on.nil?
end
create_without_timestamps
end
diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb
index 1e0185d39c..aaa298dc49 100644
--- a/activerecord/lib/active_record/transactions.rb
+++ b/activerecord/lib/active_record/transactions.rb
@@ -200,7 +200,7 @@ module ActiveRecord
end
def save_with_transactions! #:nodoc:
- rollback_active_record_state! { transaction { save_without_transactions! } }
+ rollback_active_record_state! { self.class.transaction { save_without_transactions! } }
end
# Reset id and @new_record if the transaction rolls back.
@@ -228,7 +228,7 @@ module ActiveRecord
# instance.
def with_transaction_returning_status(method, *args)
status = nil
- transaction do
+ self.class.transaction do
status = send(method, *args)
raise ActiveRecord::Rollback unless status
end
diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb
index 4b275ddd70..6a9690ba85 100644
--- a/activerecord/lib/active_record/validations.rb
+++ b/activerecord/lib/active_record/validations.rb
@@ -203,7 +203,6 @@ module ActiveRecord
if attr == "base"
full_messages << message
else
- #key = :"activerecord.att.#{@base.class.name.underscore.to_sym}.#{attr}"
attr_name = @base.class.human_attribute_name(attr)
full_messages << attr_name + I18n.t('activerecord.errors.format.separator', :default => ' ') + message
end
@@ -1049,15 +1048,15 @@ module ActiveRecord
protected
# Overwrite this method for validation checks on all saves and use <tt>Errors.add(field, msg)</tt> for invalid attributes.
- def validate #:doc:
+ def validate
end
# Overwrite this method for validation checks used only on creation.
- def validate_on_create #:doc:
+ def validate_on_create
end
# Overwrite this method for validation checks used only on updates.
- def validate_on_update # :doc:
+ def validate_on_update
end
end
end