Instiki 0.16.3: Rails 2.3.0

Instiki now runs on the Rails 2.3.0 Candidate Release.
Among other improvements, this means that it now 
automagically selects between WEBrick and Mongrel.

Just run

    ./instiki --daemon
This commit is contained in:
Jacques Distler 2009-02-04 14:26:08 -06:00
parent 43aadecc99
commit 4e14ccc74d
893 changed files with 71965 additions and 28511 deletions

View file

@ -1,5 +1,5 @@
#--
# Copyright (c) 2004-2008 David Heinemeier Hansson
# Copyright (c) 2004-2009 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
@ -31,51 +31,53 @@ rescue LoadError
end
end
require 'active_record/base'
require 'active_record/named_scope'
require 'active_record/observer'
require 'active_record/query_cache'
require 'active_record/validations'
require 'active_record/callbacks'
require 'active_record/reflection'
require 'active_record/associations'
require 'active_record/association_preload'
require 'active_record/aggregations'
require 'active_record/transactions'
require 'active_record/timestamp'
require 'active_record/locking/optimistic'
require 'active_record/locking/pessimistic'
require 'active_record/migration'
require 'active_record/schema'
require 'active_record/calculations'
require 'active_record/serialization'
require 'active_record/attribute_methods'
require 'active_record/dirty'
require 'active_record/dynamic_finder_match'
module ActiveRecord
# TODO: Review explicit loads to see if they will automatically be handled by the initilizer.
def self.load_all!
[Base, DynamicFinderMatch, ConnectionAdapters::AbstractAdapter]
end
ActiveRecord::Base.class_eval do
extend ActiveRecord::QueryCache
include ActiveRecord::Validations
include ActiveRecord::Locking::Optimistic
include ActiveRecord::Locking::Pessimistic
include ActiveRecord::AttributeMethods
include ActiveRecord::Dirty
include ActiveRecord::Callbacks
include ActiveRecord::Observing
include ActiveRecord::Timestamp
include ActiveRecord::Associations
include ActiveRecord::NamedScope
include ActiveRecord::AssociationPreload
include ActiveRecord::Aggregations
include ActiveRecord::Transactions
include ActiveRecord::Reflection
include ActiveRecord::Calculations
include ActiveRecord::Serialization
autoload :VERSION, 'active_record/version'
autoload :ActiveRecordError, 'active_record/base'
autoload :ConnectionNotEstablished, 'active_record/base'
autoload :Aggregations, 'active_record/aggregations'
autoload :AssociationPreload, 'active_record/association_preload'
autoload :Associations, 'active_record/associations'
autoload :AttributeMethods, 'active_record/attribute_methods'
autoload :AutosaveAssociation, 'active_record/autosave_association'
autoload :Base, 'active_record/base'
autoload :Calculations, 'active_record/calculations'
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'
autoload :NestedAttributes, 'active_record/nested_attributes'
autoload :Observing, 'active_record/observer'
autoload :QueryCache, 'active_record/query_cache'
autoload :Reflection, 'active_record/reflection'
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'
autoload :Validations, 'active_record/validations'
module Locking
autoload :Optimistic, 'active_record/locking/optimistic'
autoload :Pessimistic, 'active_record/locking/pessimistic'
end
module ConnectionAdapters
autoload :AbstractAdapter, 'active_record/connection_adapters/abstract_adapter'
end
end
require 'active_record/connection_adapters/abstract_adapter'
require 'active_record/schema_dumper'
require 'active_record/i18n_interpolation_deprecation'
I18n.load_path << File.dirname(__FILE__) + '/active_record/locale/en.yml'

View file

@ -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]} as 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

View file

@ -1,13 +1,3 @@
require 'active_record/associations/association_proxy'
require 'active_record/associations/association_collection'
require 'active_record/associations/belongs_to_association'
require 'active_record/associations/belongs_to_polymorphic_association'
require 'active_record/associations/has_one_association'
require 'active_record/associations/has_many_association'
require 'active_record/associations/has_many_through_association'
require 'active_record/associations/has_and_belongs_to_many_association'
require 'active_record/associations/has_one_through_association'
module ActiveRecord
class HasManyThroughAssociationNotFoundError < ActiveRecordError #:nodoc:
def initialize(owner_class_name, reflection)
@ -32,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
@ -75,6 +65,18 @@ module ActiveRecord
# See ActiveRecord::Associations::ClassMethods for documentation.
module Associations # :nodoc:
# These classes will be loaded when associatoins are created.
# So there is no need to eager load them.
autoload :AssociationCollection, 'active_record/associations/association_collection'
autoload :AssociationProxy, 'active_record/associations/association_proxy'
autoload :BelongsToAssociation, 'active_record/associations/belongs_to_association'
autoload :BelongsToPolymorphicAssociation, 'active_record/associations/belongs_to_polymorphic_association'
autoload :HasAndBelongsToManyAssociation, 'active_record/associations/has_and_belongs_to_many_association'
autoload :HasManyAssociation, 'active_record/associations/has_many_association'
autoload :HasManyThroughAssociation, 'active_record/associations/has_many_through_association'
autoload :HasOneAssociation, 'active_record/associations/has_one_association'
autoload :HasOneThroughAssociation, 'active_record/associations/has_one_through_association'
def self.included(base)
base.extend(ClassMethods)
end
@ -86,6 +88,18 @@ module ActiveRecord
end unless self.new_record?
end
private
# Gets the specified association instance if it responds to :loaded?, nil otherwise.
def association_instance_get(name)
association = instance_variable_get("@#{name}")
association if association.respond_to?(:loaded?)
end
# Set the specified association instance.
def association_instance_set(name, association)
instance_variable_set("@#{name}", association)
end
# Associations are a set of macro-like class methods for tying objects together through foreign keys. They express relationships like
# "Project has one Project Manager" or "Project belongs to a Portfolio". Each macro adds a number of methods to the class which are
# specialized according to the collection or association symbol and the options hash. It works much the same way as Ruby's own <tt>attr*</tt>
@ -151,7 +165,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
#
@ -254,6 +268,10 @@ module ActiveRecord
# You can manipulate objects and associations before they are saved to the database, but there is some special behavior you should be
# aware of, mostly involving the saving of associated objects.
#
# Unless you enable the :autosave option on a <tt>has_one</tt>, <tt>belongs_to</tt>,
# <tt>has_many</tt>, or <tt>has_and_belongs_to_many</tt> association,
# in which case the members are always saved.
#
# === One-to-one associations
#
# * Assigning an object to a +has_one+ association automatically saves that object and the object being replaced (if there is one), in
@ -650,7 +668,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 = {}, ...)]
@ -680,7 +698,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.
@ -722,6 +740,8 @@ module ActiveRecord
# Specify second-order associations that should be eager loaded when the collection is loaded.
# [:group]
# An attribute name by which the result should be grouped. Uses the <tt>GROUP BY</tt> SQL-clause.
# [:having]
# Combined with +:group+ this can be used to filter the records that a <tt>GROUP BY</tt> returns. Uses the <tt>HAVING</tt> SQL-clause.
# [:limit]
# An integer determining the limit on the number of rows that should be returned.
# [:offset]
@ -748,6 +768,9 @@ module ActiveRecord
# If true, all the associated objects are readonly through the association.
# [:validate]
# If false, don't validate the associated objects when saving the parent object. true by default.
# [:autosave]
# If true, always save any loaded members and destroy members marked for destruction, when saving the parent object. Off by default.
#
# Option examples:
# has_many :comments, :order => "posted_on"
# has_many :comments, :include => :author
@ -861,6 +884,8 @@ module ActiveRecord
# If true, the associated object is readonly through the association.
# [:validate]
# If false, don't validate the associated object when saving the parent object. +false+ by default.
# [:autosave]
# If true, always save the associated object or destroy it if marked for destruction, when saving the parent object. Off by default.
#
# Option examples:
# has_one :credit_card, :dependent => :destroy # destroys the associated credit card
@ -878,13 +903,10 @@ module ActiveRecord
else
reflection = create_has_one_reflection(association_id, options)
ivar = "@#{reflection.name}"
method_name = "has_one_after_save_for_#{reflection.name}".to_sym
define_method(method_name) do
association = instance_variable_get(ivar) if instance_variable_defined?(ivar)
if !association.nil? && (new_record? || association.new_record? || association[reflection.primary_key_name] != id)
association = association_instance_get(reflection.name)
if association && (new_record? || association.new_record? || association[reflection.primary_key_name] != id)
association[reflection.primary_key_name] = id
association.save(true)
end
@ -975,6 +997,8 @@ module ActiveRecord
# If true, the associated object is readonly through the association.
# [:validate]
# If false, don't validate the associated objects when saving the parent object. +false+ by default.
# [:autosave]
# If true, always save the associated object or destroy it if marked for destruction, when saving the parent object. Off by default.
#
# Option examples:
# belongs_to :firm, :foreign_key => "client_of"
@ -987,15 +1011,12 @@ module ActiveRecord
def belongs_to(association_id, options = {})
reflection = create_belongs_to_reflection(association_id, options)
ivar = "@#{reflection.name}"
if reflection.options[:polymorphic]
association_accessor_methods(reflection, BelongsToPolymorphicAssociation)
method_name = "polymorphic_belongs_to_before_save_for_#{reflection.name}".to_sym
define_method(method_name) do
association = instance_variable_get(ivar) if instance_variable_defined?(ivar)
association = association_instance_get(reflection.name)
if association && association.target
if association.new_record?
association.save(true)
@ -1015,9 +1036,7 @@ module ActiveRecord
method_name = "belongs_to_before_save_for_#{reflection.name}".to_sym
define_method(method_name) do
association = instance_variable_get(ivar) if instance_variable_defined?(ivar)
if !association.nil?
if association = association_instance_get(reflection.name)
if association.new_record?
association.save(true)
end
@ -1103,7 +1122,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 = {})]
@ -1129,7 +1148,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.
@ -1147,11 +1166,12 @@ module ActiveRecord
# [:foreign_key]
# Specify the foreign key used for the association. By default this is guessed to be the name
# of this class in lower-case and "_id" suffixed. So a Person class that makes a +has_and_belongs_to_many+ association
# will use "person_id" as the default <tt>:foreign_key</tt>.
# to Project will use "person_id" as the default <tt>:foreign_key</tt>.
# [:association_foreign_key]
# Specify the association foreign key used for the association. By default this is
# guessed to be the name of the associated class in lower-case and "_id" suffixed. So if the associated class is Project,
# the +has_and_belongs_to_many+ association will use "project_id" as the default <tt>:association_foreign_key</tt>.
# Specify the foreign key used for the association on the receiving side of the association.
# By default this is guessed to be the name of the associated class in lower-case and "_id" suffixed.
# So if a Person class makes a +has_and_belongs_to_many+ association to Project,
# the association will use "project_id" as the default <tt>:association_foreign_key</tt>.
# [:conditions]
# Specify the conditions that the associated object must meet in order to be included as a +WHERE+
# SQL fragment, such as <tt>authorized = 1</tt>. Record creations from the association are scoped if a hash is used.
@ -1179,6 +1199,8 @@ module ActiveRecord
# Specify second-order associations that should be eager loaded when the collection is loaded.
# [:group]
# An attribute name by which the result should be grouped. Uses the <tt>GROUP BY</tt> SQL-clause.
# [:having]
# Combined with +:group+ this can be used to filter the records that a <tt>GROUP BY</tt> returns. Uses the <tt>HAVING</tt> SQL-clause.
# [:limit]
# An integer determining the limit on the number of rows that should be returned.
# [:offset]
@ -1190,6 +1212,8 @@ module ActiveRecord
# If true, all the associated objects are readonly through the association.
# [:validate]
# If false, don't validate the associated objects when saving the parent object. +true+ by default.
# [:autosave]
# If true, always save any loaded members and destroy members marked for destruction, when saving the parent object. Off by default.
#
# Option examples:
# has_and_belongs_to_many :projects
@ -1210,11 +1234,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)
@ -1237,33 +1261,30 @@ module ActiveRecord
end
def association_accessor_methods(reflection, association_proxy_class)
ivar = "@#{reflection.name}"
define_method(reflection.name) do |*params|
force_reload = params.first unless params.empty?
association = instance_variable_get(ivar) if instance_variable_defined?(ivar)
association = association_instance_get(reflection.name)
if association.nil? || force_reload
association = association_proxy_class.new(self, reflection)
retval = association.reload
if retval.nil? and association_proxy_class == BelongsToAssociation
instance_variable_set(ivar, nil)
association_instance_set(reflection.name, nil)
return nil
end
instance_variable_set(ivar, association)
association_instance_set(reflection.name, association)
end
association.target.nil? ? nil : association
end
define_method("loaded_#{reflection.name}?") do
association = instance_variable_get(ivar) if instance_variable_defined?(ivar)
association = association_instance_get(reflection.name)
association && association.loaded?
end
define_method("#{reflection.name}=") do |new_value|
association = instance_variable_get(ivar) if instance_variable_defined?(ivar)
association = association_instance_get(reflection.name)
if association.nil? || association.target != new_value
association = association_proxy_class.new(self, reflection)
@ -1274,7 +1295,7 @@ module ActiveRecord
self.send(reflection.name, new_value)
else
association.replace(new_value)
instance_variable_set(ivar, new_value.nil? ? nil : association)
association_instance_set(reflection.name, new_value.nil? ? nil : association)
end
end
@ -1282,20 +1303,18 @@ module ActiveRecord
return if target.nil? and association_proxy_class == BelongsToAssociation
association = association_proxy_class.new(self, reflection)
association.target = target
instance_variable_set(ivar, association)
association_instance_set(reflection.name, association)
end
end
def collection_reader_method(reflection, association_proxy_class)
define_method(reflection.name) do |*params|
ivar = "@#{reflection.name}"
force_reload = params.first unless params.empty?
association = instance_variable_get(ivar) if instance_variable_defined?(ivar)
association = association_instance_get(reflection.name)
unless association.respond_to?(:loaded?)
unless association
association = association_proxy_class.new(self, reflection)
instance_variable_set(ivar, association)
association_instance_set(reflection.name, association)
end
association.reload if force_reload
@ -1333,8 +1352,7 @@ module ActiveRecord
def add_single_associated_validation_callbacks(association_name)
method_name = "validate_associated_records_for_#{association_name}".to_sym
define_method(method_name) do
association = instance_variable_get("@#{association_name}")
if !association.nil?
if association = association_instance_get(association_name)
errors.add association_name unless association.target.nil? || association.valid?
end
end
@ -1344,12 +1362,10 @@ module ActiveRecord
def add_multiple_associated_validation_callbacks(association_name)
method_name = "validate_associated_records_for_#{association_name}".to_sym
ivar = "@#{association_name}"
define_method(method_name) do
association = instance_variable_get(ivar) if instance_variable_defined?(ivar)
association = association_instance_get(association_name)
if association.respond_to?(:loaded?)
if association
if new_record?
association
elsif association.loaded?
@ -1366,8 +1382,6 @@ module ActiveRecord
end
def add_multiple_associated_save_callbacks(association_name)
ivar = "@#{association_name}"
method_name = "before_save_associated_records_for_#{association_name}".to_sym
define_method(method_name) do
@new_record_before_save = new_record?
@ -1377,13 +1391,13 @@ module ActiveRecord
method_name = "after_create_or_update_associated_records_for_#{association_name}".to_sym
define_method(method_name) do
association = instance_variable_get(ivar) if instance_variable_defined?(ivar)
association = association_instance_get(association_name)
records_to_save = if @new_record_before_save
association
elsif association.respond_to?(:loaded?) && association.loaded?
elsif association && association.loaded?
association.select { |record| record.new_record? }
elsif association.respond_to?(:loaded?) && !association.loaded?
elsif association && !association.loaded?
association.target.select { |record| record.new_record? }
else
[]
@ -1401,15 +1415,13 @@ module ActiveRecord
def association_constructor_method(constructor, reflection, association_proxy_class)
define_method("#{constructor}_#{reflection.name}") do |*params|
ivar = "@#{reflection.name}"
attributees = params.first unless params.empty?
replace_existing = params[1].nil? ? true : params[1]
association = instance_variable_get(ivar) if instance_variable_defined?(ivar)
association = association_instance_get(reflection.name)
if association.nil?
unless association
association = association_proxy_class.new(self, reflection)
instance_variable_set(ivar, association)
association_instance_set(reflection.name, association)
end
if association_proxy_class == HasOneAssociation
@ -1447,7 +1459,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
@ -1457,22 +1469,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})"
@ -1525,14 +1537,14 @@ module ActiveRecord
association = send(reflection.name)
association.destroy unless association.nil?
end
before_destroy method_name
after_destroy method_name
when :delete
method_name = "belongs_to_dependent_delete_for_#{reflection.name}".to_sym
define_method(method_name) do
association = send(reflection.name)
association.delete unless association.nil?
end
before_destroy method_name
after_destroy method_name
else
raise ArgumentError, "The :dependent option expects either :destroy or :delete (#{reflection.options[:dependent].inspect})"
end
@ -1551,7 +1563,7 @@ module ActiveRecord
@@valid_keys_for_has_many_association = [
:class_name, :table_name, :foreign_key, :primary_key,
:dependent,
:select, :conditions, :include, :order, :group, :limit, :offset,
:select, :conditions, :include, :order, :group, :having, :limit, :offset,
:as, :through, :source, :source_type,
:uniq,
:finder_sql, :counter_sql,
@ -1607,7 +1619,7 @@ module ActiveRecord
mattr_accessor :valid_keys_for_has_and_belongs_to_many_association
@@valid_keys_for_has_and_belongs_to_many_association = [
:class_name, :table_name, :join_table, :foreign_key, :association_foreign_key,
:select, :conditions, :include, :order, :group, :limit, :offset,
:select, :conditions, :include, :order, :group, :having, :limit, :offset,
:uniq,
:finder_sql, :counter_sql, :delete_sql, :insert_sql,
:before_add, :after_add, :before_remove, :after_remove,
@ -1656,7 +1668,7 @@ module ActiveRecord
add_conditions!(sql, options[:conditions], scope)
add_limited_ids_condition!(sql, options, join_dependency) if !using_limitable_reflections?(join_dependency.reflections) && ((scope && scope[:limit]) || options[:limit])
add_group!(sql, options[:group], scope)
add_group!(sql, options[:group], options[:having], scope)
add_order!(sql, options[:order], scope)
add_limit!(sql, options, scope) if using_limitable_reflections?(join_dependency.reflections)
add_lock!(sql, options, scope)
@ -1712,7 +1724,7 @@ module ActiveRecord
end
add_conditions!(sql, options[:conditions], scope)
add_group!(sql, options[:group], scope)
add_group!(sql, options[:group], options[:having], scope)
if order && is_distinct
connection.add_order_by_for_association_limiting!(sql, :order => order)
@ -1725,46 +1737,70 @@ 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|
case cond
when nil then all
when Array then all << cond.first
when Hash then all << cond.keys
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)
@ -2141,7 +2177,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
@ -2168,7 +2204,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

View file

@ -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

View file

@ -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
@ -188,6 +191,7 @@ module ActiveRecord
def merge_options_from_reflection!(options)
options.reverse_merge!(
:group => @reflection.options[:group],
:having => @reflection.options[:having],
:limit => @reflection.options[:limit],
:offset => @reflection.options[:offset],
:joins => @reflection.options[:joins],
@ -206,7 +210,10 @@ module ActiveRecord
# Forwards any missing method call to the \target.
def method_missing(method, *args)
if load_target
raise NoMethodError unless @target.respond_to?(method)
unless @target.respond_to?(method)
message = "undefined method `#{method.to_s}' for \"#{@target}\":#{@target.class.to_s}"
raise NoMethodError, message
end
if block_given?
@target.send(method, *args) { |*block_args| yield(*block_args) }

View file

@ -9,6 +9,14 @@ module ActiveRecord
create_record(attributes) { |record| insert_record(record, true) }
end
def columns
@reflection.columns(@reflection.options[:join_table], "#{@reflection.options[:join_table]} Columns")
end
def reset_column_information
@reflection.reset_column_information
end
protected
def construct_find_options!(options)
options[:joins] = @join_sql
@ -32,8 +40,6 @@ module ActiveRecord
if @reflection.options[:insert_sql]
@owner.connection.insert(interpolate_sql(@reflection.options[:insert_sql], record))
else
columns = @owner.connection.columns(@reflection.options[:join_table], "#{@reflection.options[:join_table]} Columns")
attributes = columns.inject({}) do |attrs, column|
case column.name.to_s
when @reflection.primary_key_name.to_s
@ -103,7 +109,7 @@ module ActiveRecord
# clause has been explicitly defined. Otherwise you can get broken records back, if, for example, the join column also has
# an id column. This will then overwrite the id column of the records coming back.
def finding_with_ambiguous_select?(select_clause)
!select_clause && @owner.connection.columns(@reflection.options[:join_table], "Join Table Columns").size != 2
!select_clause && columns.size != 2
end
private

View file

@ -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

View file

@ -0,0 +1,213 @@
module ActiveRecord
# AutosaveAssociation is a module that takes care of automatically saving
# your associations when the parent is saved. In addition to saving, it
# also destroys any associations that were marked for destruction.
# (See mark_for_destruction and marked_for_destruction?)
#
# Saving of the parent, its associations, and the destruction of marked
# associations, all happen inside 1 transaction. This should never leave the
# database in an inconsistent state after, for instance, mass assigning
# attributes and saving them.
#
# If validations for any of the associations fail, their error messages will
# be applied to the parent.
#
# Note that it also means that associations marked for destruction won't
# be destroyed directly. They will however still be marked for destruction.
#
# === One-to-one Example
#
# Consider a Post model with one Author:
#
# class Post
# has_one :author, :autosave => true
# end
#
# Saving changes to the parent and its associated model can now be performed
# automatically _and_ atomically:
#
# post = Post.find(1)
# post.title # => "The current global position of migrating ducks"
# post.author.name # => "alloy"
#
# post.title = "On the migration of ducks"
# post.author.name = "Eloy Duran"
#
# post.save
# post.reload
# post.title # => "On the migration of ducks"
# post.author.name # => "Eloy Duran"
#
# Destroying an associated model, as part of the parent's save action, is as
# simple as marking it for destruction:
#
# post.author.mark_for_destruction
# post.author.marked_for_destruction? # => true
#
# Note that the model is _not_ yet removed from the database:
# id = post.author.id
# Author.find_by_id(id).nil? # => false
#
# post.save
# post.reload.author # => nil
#
# Now it _is_ removed from the database:
# Author.find_by_id(id).nil? # => true
#
# === One-to-many Example
#
# Consider a Post model with many Comments:
#
# class Post
# has_many :comments, :autosave => true
# end
#
# Saving changes to the parent and its associated model can now be performed
# automatically _and_ atomically:
#
# post = Post.find(1)
# post.title # => "The current global position of migrating ducks"
# post.comments.first.body # => "Wow, awesome info thanks!"
# post.comments.last.body # => "Actually, your article should be named differently."
#
# post.title = "On the migration of ducks"
# post.comments.last.body = "Actually, your article should be named differently. [UPDATED]: You are right, thanks."
#
# post.save
# post.reload
# post.title # => "On the migration of ducks"
# post.comments.last.body # => "Actually, your article should be named differently. [UPDATED]: You are right, thanks."
#
# Destroying one of the associated models members, as part of the parent's
# save action, is as simple as marking it for destruction:
#
# post.comments.last.mark_for_destruction
# post.comments.last.marked_for_destruction? # => true
# post.comments.length # => 2
#
# Note that the model is _not_ yet removed from the database:
# id = post.comments.last.id
# Comment.find_by_id(id).nil? # => false
#
# post.save
# post.reload.comments.length # => 1
#
# Now it _is_ removed from the database:
# Comment.find_by_id(id).nil? # => true
#
# === Validation
#
# Validation is performed on the parent as usual, but also on all autosave
# enabled associations. If any of the associations fail validation, its
# error messages will be applied on the parents errors object and validation
# of the parent will fail.
#
# Consider a Post model with Author which validates the presence of its name
# attribute:
#
# class Post
# has_one :author, :autosave => true
# end
#
# class Author
# validates_presence_of :name
# end
#
# post = Post.find(1)
# post.author.name = ''
# post.save # => false
# post.errors # => #<ActiveRecord::Errors:0x174498c @errors={"author_name"=>["can't be blank"]}, @base=#<Post ...>>
#
# No validations will be performed on the associated models when validations
# are skipped for the parent:
#
# post = Post.find(1)
# post.author.name = ''
# post.save(false) # => true
module AutosaveAssociation
def self.included(base)
base.class_eval do
alias_method_chain :reload, :autosave_associations
alias_method_chain :save, :autosave_associations
alias_method_chain :valid?, :autosave_associations
%w{ has_one belongs_to has_many has_and_belongs_to_many }.each do |type|
base.send("valid_keys_for_#{type}_association") << :autosave
end
end
end
# Saves the parent, <tt>self</tt>, and any loaded autosave associations.
# In addition, it destroys all children that were marked for destruction
# with mark_for_destruction.
#
# This all happens inside a transaction, _if_ the Transactions module is included into
# ActiveRecord::Base after the AutosaveAssociation module, which it does by default.
def save_with_autosave_associations(perform_validation = true)
returning(save_without_autosave_associations(perform_validation)) do |valid|
if valid
self.class.reflect_on_all_autosave_associations.each do |reflection|
if (association = association_instance_get(reflection.name)) && association.loaded?
if association.is_a?(Array)
association.proxy_target.each do |child|
child.marked_for_destruction? ? child.destroy : child.save(perform_validation)
end
else
association.marked_for_destruction? ? association.destroy : association.save(perform_validation)
end
end
end
end
end
end
# Returns whether or not the parent, <tt>self</tt>, and any loaded autosave associations are valid.
def valid_with_autosave_associations?
if valid_without_autosave_associations?
self.class.reflect_on_all_autosave_associations.all? do |reflection|
if (association = association_instance_get(reflection.name)) && association.loaded?
if association.is_a?(Array)
association.proxy_target.all? { |child| autosave_association_valid?(reflection, child) }
else
autosave_association_valid?(reflection, association)
end
else
true # association not loaded yet, so it should be valid
end
end
else
false # self was not valid
end
end
# Returns whether or not the association is valid and applies any errors to the parent, <tt>self</tt>, if it wasn't.
def autosave_association_valid?(reflection, association)
returning(association.valid?) do |valid|
association.errors.each do |attribute, message|
errors.add "#{reflection.name}_#{attribute}", message
end unless valid
end
end
# Reloads the attributes of the object as usual and removes a mark for destruction.
def reload_with_autosave_associations(options = nil)
@marked_for_destruction = false
reload_without_autosave_associations(options)
end
# Marks this record to be destroyed as part of the parents save transaction.
# This does _not_ actually destroy the record yet, rather it will be destroyed when <tt>parent.save</tt> is called.
#
# Only useful if the <tt>:autosave</tt> option on the parent is enabled for this associated model.
def mark_for_destruction
@marked_for_destruction = true
end
# Returns whether or not this record will be destroyed as part of the parents save transaction.
#
# Only useful if the <tt>:autosave</tt> option on the parent is enabled for this associated model.
def marked_for_destruction?
@marked_for_destruction
end
end
end

View file

@ -327,7 +327,7 @@ module ActiveRecord #:nodoc:
# User.find(user.id).preferences # => { "background" => "black", "display" => large }
#
# You can also specify a class option as the second parameter that'll raise an exception if a serialized object is retrieved as a
# descendent of a class not in the hierarchy. Example:
# descendant of a class not in the hierarchy. Example:
#
# class User < ActiveRecord::Base
# serialize :preferences, Hash
@ -389,6 +389,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
@ -414,7 +416,9 @@ module ActiveRecord #:nodoc:
end
@@subclasses = {}
##
# :singleton-method:
# Contains the database configuration - as is typically stored in config/database.yml -
# as a Hash.
#
@ -443,6 +447,8 @@ module ActiveRecord #:nodoc:
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
@ -450,34 +456,46 @@ module ActiveRecord #:nodoc:
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.
cattr_accessor :table_name_prefix, :instance_writer => false
@@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.
cattr_accessor :table_name_suffix, :instance_writer => false
@@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.
cattr_accessor :pluralize_table_names, :instance_writer => false
@@pluralize_table_names = true
##
# :singleton-method:
# Determines whether to use ANSI codes to colorize the logging statements committed by the connection adapter. These colors
# make it much easier to overview things during debugging (when used through a reader like +tail+ and on a black background), but
# may complicate matters if you use software like syslog. This is true, by default.
cattr_accessor :colorize_logging, :instance_writer => false
@@colorize_logging = 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
@ -487,6 +505,8 @@ module ActiveRecord #:nodoc:
cattr_accessor :schema_format , :instance_writer => false
@@schema_format = :ruby
##
# :singleton-method:
# Specify whether or not to use timestamps for migration numbers
cattr_accessor :timestamped_migrations , :instance_writer => false
@@timestamped_migrations = true
@ -495,6 +515,10 @@ module ActiveRecord #:nodoc:
superclass_delegating_accessor :store_full_sti_class
self.store_full_sti_class = false
# Stores the default scope for the class
class_inheritable_accessor :default_scoping, :instance_writer => false
self.default_scoping = []
class << self # Class methods
# Find operates with four different retrieval approaches:
#
@ -517,10 +541,12 @@ module ActiveRecord #:nodoc:
# * <tt>:conditions</tt> - An SQL fragment like "administrator = 1", <tt>[ "user_name = ?", username ]</tt>, or <tt>["user_name = :user_name", { :user_name => user_name }]</tt>. See conditions in the intro.
# * <tt>:order</tt> - An SQL fragment like "created_at DESC, name".
# * <tt>:group</tt> - An attribute name by which the result should be grouped. Uses the <tt>GROUP BY</tt> SQL-clause.
# * <tt>:having</tt> - Combined with +:group+ this can be used to filter the records that a <tt>GROUP BY</tt> returns. Uses the <tt>HAVING</tt> SQL-clause.
# * <tt>:limit</tt> - An integer determining the limit on the number of rows that should be returned.
# * <tt>:offset</tt> - An integer determining the offset from where the rows should be fetched. So at 5, it would skip rows 0 through 4.
# * <tt>:joins</tt> - Either an SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id" (rarely needed)
# or named associations in the same form used for the <tt>:include</tt> option, which will perform an <tt>INNER JOIN</tt> on the associated table(s).
# * <tt>:joins</tt> - Either an SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id" (rarely needed),
# named associations in the same form used for the <tt>:include</tt> option, which will perform an <tt>INNER JOIN</tt> on the associated table(s),
# or an array containing a mixture of both strings and named associations.
# If the value is a string, then the records will be returned read-only since they will have attributes that do not correspond to the table's columns.
# Pass <tt>:readonly => false</tt> to override.
# * <tt>:include</tt> - Names associations that should be loaded alongside. The symbols named refer
@ -635,16 +661,24 @@ module ActiveRecord #:nodoc:
connection.select_all(sanitize_sql(sql), "#{name} Load").collect! { |record| instantiate(record) }
end
# Checks whether a record exists in the database that matches conditions given. These conditions
# can either be a single integer representing a primary key id to be found, or a condition to be
# matched like using ActiveRecord#find.
# Returns true if a record exists in the table that matches the +id+ or
# conditions given, or false otherwise. The argument can take four forms:
#
# The +id_or_conditions+ parameter can be an Integer or a String if you want to search the primary key
# column of the table for a matching id, or if you're looking to match against a condition you can use
# an Array or a Hash.
# * Integer - Finds the record with this primary key.
# * String - Finds the record with a primary key corresponding to this
# string (such as <tt>'5'</tt>).
# * Array - Finds the record that matches these +find+-style conditions
# (such as <tt>['color = ?', 'red']</tt>).
# * Hash - Finds the record that matches these +find+-style conditions
# (such as <tt>{:color => 'red'}</tt>).
#
# Possible gotcha: You can't pass in a condition as a string e.g. "name = 'Jamie'", this would be
# sanitized and then queried against the primary key column as "id = 'name = \'Jamie"
# For more information about specifying conditions as a Hash or Array,
# see the Conditions section in the introduction to ActiveRecord::Base.
#
# Note: You can't pass in a condition as a string (like <tt>name =
# 'Jamie'</tt>), since it would be sanitized and then queried against
# the primary key column, like <tt>id = 'name = \'Jamie\''</tt>.
#
# ==== Examples
# Person.exists?(5)
@ -722,25 +756,26 @@ module ActiveRecord #:nodoc:
end
end
# Delete an object (or multiple objects) where the +id+ given matches the primary_key. A SQL +DELETE+ command
# is executed on the database which means that no callbacks are fired off running this. This is an efficient method
# of deleting records that don't need cleaning up after or other actions to be taken.
# Deletes the row with a primary key matching the +id+ argument, using a
# SQL +DELETE+ statement, and returns the number of rows deleted. Active
# Record objects are not instantiated, so the object's callbacks are not
# executed, including any <tt>:dependent</tt> association options or
# Observer methods.
#
# Objects are _not_ instantiated with this method, and so +:dependent+ rules
# defined on associations are not honered.
# You can delete multiple rows at once by passing an Array of <tt>id</tt>s.
#
# ==== Parameters
#
# * +id+ - Can be either an Integer or an Array of Integers.
# Note: Although it is often much faster than the alternative,
# <tt>#destroy</tt>, skipping callbacks might bypass business logic in
# your application that ensures referential integrity or performs other
# essential jobs.
#
# ==== Examples
#
# # Delete a single object
# # Delete a single row
# Todo.delete(1)
#
# # Delete multiple objects
# todos = [1,2,3]
# Todo.delete(todos)
# # Delete multiple rows
# Todo.delete([2,3,4])
def delete(id)
delete_all([ "#{connection.quote_column_name(primary_key)} IN (?)", id ])
end
@ -778,8 +813,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.
#
@ -817,25 +851,32 @@ module ActiveRecord #:nodoc:
connection.update(sql, "#{name} Update")
end
# Destroys the records matching +conditions+ by instantiating each record and calling their +destroy+ method.
# This means at least 2*N database queries to destroy N records, so avoid +destroy_all+ if you are deleting
# many records. If you want to simply delete records without worrying about dependent associations or
# callbacks, use the much faster +delete_all+ method instead.
# Destroys the records matching +conditions+ by instantiating each
# record and calling its +destroy+ method. Each object's callbacks are
# executed (including <tt>:dependent</tt> association options and
# +before_destroy+/+after_destroy+ Observer methods). Returns the
# collection of objects that were destroyed; each will be frozen, to
# reflect that no changes should be made (since they can't be
# persisted).
#
# Note: Instantiation, callback execution, and deletion of each
# record can be time consuming when you're removing many records at
# once. It generates at least one SQL +DELETE+ query per record (or
# possibly more, to enforce your callbacks). If you want to delete many
# rows quickly, without concern for their associations or callbacks, use
# +delete_all+ instead.
#
# ==== Parameters
#
# * +conditions+ - Conditions are specified the same way as with +find+ method.
# * +conditions+ - A string, array, or hash that specifies which records
# to destroy. If omitted, all records are destroyed. See the
# Conditions section in the introduction to ActiveRecord::Base for
# more information.
#
# ==== Example
# ==== Examples
#
# Person.destroy_all("last_login < '2004-04-04'")
#
# This loads and destroys each person one by one, including its dependent associations and before_ and
# after_destroy callbacks.
#
# +conditions+ can be anything that +find+ also accepts:
#
# Person.destroy_all(:last_login => 6.hours.ago)
# Person.destroy_all(:status => "inactive")
def destroy_all(conditions = nil)
find(:all, :conditions => conditions).each { |object| object.destroy }
end
@ -886,7 +927,7 @@ module ActiveRecord #:nodoc:
#
# ==== Parameters
#
# * +id+ - The id of the object you wish to update a counter on.
# * +id+ - The id of the object you wish to update a counter on or an Array of ids.
# * +counters+ - An Array of Hashes containing the names of the fields
# to update as keys and the amount to update the field by as values.
#
@ -900,12 +941,27 @@ module ActiveRecord #:nodoc:
# # SET comment_count = comment_count - 1,
# # action_count = action_count + 1
# # WHERE id = 5
#
# # For the Posts with id of 10 and 15, increment the comment_count by 1
# Post.update_counters [10, 15], :comment_count => 1
# # Executes the following SQL:
# # UPDATE posts
# # SET comment_count = comment_count + 1,
# # WHERE id IN (10, 15)
def update_counters(id, counters)
updates = counters.inject([]) { |list, (counter_name, increment)|
sign = increment < 0 ? "-" : "+"
list << "#{connection.quote_column_name(counter_name)} = COALESCE(#{connection.quote_column_name(counter_name)}, 0) #{sign} #{increment.abs}"
}.join(", ")
update_all(updates, "#{connection.quote_column_name(primary_key)} = #{quote_value(id)}")
if id.is_a?(Array)
ids_list = id.map {|i| quote_value(i)}.join(', ')
condition = "IN (#{ids_list})"
else
condition = "= #{quote_value(id)}"
end
update_all(updates, "#{connection.quote_column_name(primary_key)} #{condition}")
end
# Increment a number field by one, usually representing a count.
@ -1384,8 +1440,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
@ -1424,7 +1480,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
@ -1462,11 +1521,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)
@ -1628,7 +1692,7 @@ module ActiveRecord #:nodoc:
add_joins!(sql, options[:joins], scope)
add_conditions!(sql, options[:conditions], scope)
add_group!(sql, options[:group], scope)
add_group!(sql, options[:group], options[:having], scope)
add_order!(sql, options[:order], scope)
add_limit!(sql, options, scope)
add_lock!(sql, options, scope)
@ -1651,7 +1715,7 @@ module ActiveRecord #:nodoc:
end
join
end
joins.flatten.uniq
joins.flatten.map{|j| j.strip}.uniq
else
joins.collect{|j| safe_to_array(j)}.flatten.uniq
end
@ -1684,13 +1748,15 @@ module ActiveRecord #:nodoc:
end
end
def add_group!(sql, group, scope = :auto)
def add_group!(sql, group, having, scope = :auto)
if group
sql << " GROUP BY #{group}"
sql << " HAVING #{having}" if having
else
scope = scope(:find) if :auto == scope
if scope && (scoped_group = scope[:group])
sql << " GROUP BY #{scoped_group}"
sql << " HAVING #{scoped_having}" if (scoped_having = scope[:having])
end
end
end
@ -1760,17 +1826,19 @@ module ActiveRecord #:nodoc:
table_name
end
# Enables dynamic finders like find_by_user_name(user_name) and find_by_user_name_and_password(user_name, password) that are turned into
# find(:first, :conditions => ["user_name = ?", user_name]) and find(:first, :conditions => ["user_name = ? AND password = ?", user_name, password])
# respectively. Also works for find(:all) by using find_all_by_amount(50) that is turned into find(:all, :conditions => ["amount = ?", 50]).
# Enables dynamic finders like <tt>find_by_user_name(user_name)</tt> and <tt>find_by_user_name_and_password(user_name, password)</tt>
# that are turned into <tt>find(:first, :conditions => ["user_name = ?", user_name])</tt> and
# <tt>find(:first, :conditions => ["user_name = ? AND password = ?", user_name, password])</tt> respectively. Also works for
# <tt>find(:all)</tt> by using <tt>find_all_by_amount(50)</tt> that is turned into <tt>find(:all, :conditions => ["amount = ?", 50])</tt>.
#
# It's even possible to use all the additional parameters to find. For example, the full interface for find_all_by_amount
# is actually find_all_by_amount(amount, options).
# 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>.
#
# 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).
# 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 or initializer/creator is also defined in the class after it is first invoked, so that future
# 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)
@ -1779,10 +1847,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)
@ -1794,12 +1883,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
@ -1829,6 +1943,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
@ -1924,7 +2054,11 @@ module ActiveRecord #:nodoc:
# end
#
# In nested scopings, all previous parameters are overwritten by the innermost rule, with the exception of
# <tt>:conditions</tt> and <tt>:include</tt> options in <tt>:find</tt>, which are merged.
# <tt>:conditions</tt>, <tt>:include</tt>, and <tt>:joins</tt> options in <tt>:find</tt>, which are merged.
#
# <tt>:joins</tt> options 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
@ -1978,7 +2112,11 @@ module ActiveRecord #:nodoc:
(hash[method].keys + params.keys).uniq.each do |key|
merge = hash[method][key] && params[key] # merge if both scopes have the same key
if key == :conditions && merge
hash[method][key] = merge_conditions(params[key], hash[method][key])
if params[key].is_a?(Hash) && hash[method][key].is_a?(Hash)
hash[method][key] = merge_conditions(hash[method][key].deep_merge(params[key]))
else
hash[method][key] = merge_conditions(params[key], hash[method][key])
end
elsif key == :include && merge
hash[method][key] = merge_includes(hash[method][key], params[key]).uniq
elsif key == :joins && merge
@ -1988,7 +2126,7 @@ module ActiveRecord #:nodoc:
end
end
else
hash[method] = params.merge(hash[method])
hash[method] = hash[method].merge(params)
end
else
hash[method] = params
@ -2016,6 +2154,16 @@ module ActiveRecord #:nodoc:
@@subclasses[self] + extra = @@subclasses[self].inject([]) {|list, subclass| list + subclass.subclasses }
end
# Sets the default options for the model. The format of the
# <tt>options</tt> argument is the same as in find.
#
# class Person < ActiveRecord::Base
# 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] : {} }
end
# Test whether the given method and optional key are scoped.
def scoped?(method, key = nil) #:nodoc:
if current_scoped_methods && (scope = current_scoped_methods[method])
@ -2031,14 +2179,14 @@ module ActiveRecord #:nodoc:
end
def scoped_methods #:nodoc:
Thread.current[:"#{self}_scoped_methods"] ||= []
Thread.current[:"#{self}_scoped_methods"] ||= self.default_scoping.dup
end
def current_scoped_methods #:nodoc:
scoped_methods.last
end
# Returns the class type of the record using the current module as a prefix. So descendents of
# 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)
modularized_name = type_name_with_module(type_name)
@ -2051,7 +2199,8 @@ module ActiveRecord #:nodoc:
end
end
# Returns the class descending directly from Active Record in the inheritance hierarchy.
# 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.superclass == Base || klass.superclass.abstract_class?
klass
@ -2245,7 +2394,7 @@ module ActiveRecord #:nodoc:
end
VALID_FIND_OPTIONS = [ :conditions, :include, :joins, :limit, :offset,
:order, :select, :readonly, :group, :from, :lock ]
:order, :select, :readonly, :group, :having, :from, :lock ]
def validate_find_options(options) #:nodoc:
options.assert_valid_keys(VALID_FIND_OPTIONS)
@ -2303,15 +2452,15 @@ module ActiveRecord #:nodoc:
# 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 Users model, and that you have a
# <tt>map.resources :users</tt> route. Normally, +users_path+ will
# construct an URI with the user object's 'id' in it:
# For example, suppose that you have a User model, and that you have a
# <tt>map.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(path) # => "/users/1"
# user_path(user) # => "/users/1"
#
# You can override +to_param+ in your model to make +users_path+ construct
# an URI using the user's name instead of the user's id:
# 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
@ -2320,7 +2469,7 @@ module ActiveRecord #:nodoc:
# end
#
# user = User.find_by_name('Phusion')
# user_path(path) # => "/users/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 = self.id) ? id.to_s : nil # Be sure to stringify the id for routes
@ -2357,9 +2506,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:
@ -2400,14 +2549,16 @@ module ActiveRecord #:nodoc:
create_or_update || raise(RecordNotSaved)
end
# Deletes the record in the database and freezes this instance to reflect that no changes should
# be made (since they can't be persisted).
# Deletes the record in the database and freezes this instance to
# reflect that no changes should be made (since they can't be
# persisted). Returns the frozen instance.
#
# Unlike #destroy, this method doesn't run any +before_delete+ and +after_delete+
# callbacks, nor will it enforce any association +:dependent+ rules.
#
# In addition to deleting this record, any defined +before_delete+ and +after_delete+
# callbacks are run, and +:dependent+ rules defined on associations are run.
# The row is simply removed with a SQL +DELETE+ statement on the
# record's primary key, and no callbacks are executed.
#
# To enforce the object's +before_destroy+ and +after_destroy+
# callbacks, Observer methods, or any <tt>:dependent</tt> association
# options, use <tt>#destroy</tt>.
def delete
self.class.delete(id) unless new_record?
freeze
@ -2608,7 +2759,19 @@ module ActiveRecord #:nodoc:
end
end
# Format attributes nicely for inspect.
# Returns an <tt>#inspect</tt>-like string for the value of the
# attribute +attr_name+. String attributes are elided after 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)
@ -2737,7 +2900,7 @@ module ActiveRecord #:nodoc:
id
end
# Sets the attribute used for single table inheritance to this class name if this is not the ActiveRecord::Base descendent.
# 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.
@ -2964,4 +3127,23 @@ module ActiveRecord #:nodoc:
value
end
end
Base.class_eval do
extend QueryCache::ClassMethods
include Validations
include Locking::Optimistic, Locking::Pessimistic
include AttributeMethods
include Dirty
include Callbacks, Observing, Timestamp
include Associations, AssociationPreload, NamedScope
# 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, Calculations, Serialization
end
end
# TODO: Remove this and make it work with LAZY flag
require 'active_record/connection_adapters/abstract_adapter'

View file

@ -48,30 +48,38 @@ module ActiveRecord
calculate(:count, *construct_count_options_from_args(*args))
end
# Calculates the average value on a given column. The value is returned as a float. See +calculate+ for examples with options.
# Calculates the average value on a given column. The value is returned as
# a float, or +nil+ if there's no row. See +calculate+ for examples with
# options.
#
# Person.average('age')
# Person.average('age') # => 35.8
def average(column_name, options = {})
calculate(:avg, column_name, options)
end
# Calculates the minimum value on a given column. The value is returned with the same data type of the column. See +calculate+ for examples with options.
# Calculates the minimum value on a given column. The value is returned
# with the same data type of the column, or +nil+ if there's no row. See
# +calculate+ for examples with options.
#
# Person.minimum('age')
# Person.minimum('age') # => 7
def minimum(column_name, options = {})
calculate(:min, column_name, options)
end
# Calculates the maximum value on a given column. The value is returned with the same data type of the column. See +calculate+ for examples with options.
# Calculates the maximum value on a given column. The value is returned
# with the same data type of the column, or +nil+ if there's no row. See
# +calculate+ for examples with options.
#
# Person.maximum('age')
# Person.maximum('age') # => 93
def maximum(column_name, options = {})
calculate(:max, column_name, options)
end
# Calculates the sum of values on a given column. The value is returned with the same data type of the column. See +calculate+ for examples with options.
# Calculates the sum of values on a given column. The value is returned
# with the same data type of the column, 0 if there's no row. See
# +calculate+ for examples with options.
#
# Person.sum('age')
# Person.sum('age') # => 4562
def sum(column_name, options = {})
calculate(:sum, column_name, options)
end

View file

@ -77,7 +77,7 @@ module ActiveRecord
#
# In that case, <tt>Reply#destroy</tt> would only run +destroy_readers+ and _not_ +destroy_author+. So, use the callback macros when
# you want to ensure that a certain callback is called for the entire hierarchy, and use the regular overwriteable methods
# when you want to leave it up to each descendent to decide whether they want to call +super+ and trigger the inherited callbacks.
# when you want to leave it up to each descendant to decide whether they want to call +super+ and trigger the inherited callbacks.
#
# *IMPORTANT:* In order for inheritance to work for the callback queues, you must specify the callbacks before specifying the
# associations. Otherwise, you might trigger the loading of a child before the parent has registered the callbacks and they won't
@ -219,8 +219,9 @@ module ActiveRecord
def after_save() end
def create_or_update_with_callbacks #:nodoc:
return false if callback(:before_save) == false
result = create_or_update_without_callbacks
callback(:after_save)
if result = create_or_update_without_callbacks
callback(:after_save)
end
result
end
private :create_or_update_with_callbacks

View file

@ -7,6 +7,8 @@ module ActiveRecord
end
end
##
# :singleton-method:
# The connection handler
cattr_accessor :connection_handler, :instance_writer => false
@@connection_handler = ConnectionAdapters::ConnectionHandler.new
@ -121,6 +123,7 @@ module ActiveRecord
connection_handler.retrieve_connection(self)
end
# Returns true if +ActiveRecord+ is connected.
def connected?
connection_handler.connected?(self)
end

View file

@ -31,13 +31,13 @@ module ActiveRecord
# Returns an array of arrays containing the field values.
# Order is the same as that returned by +columns+.
def select_rows(sql, name = nil)
raise NotImplementedError, "select_rows is an abstract method"
end
undef_method :select_rows
# Executes the SQL statement in the context of this connection.
def execute(sql, name = nil)
raise NotImplementedError, "execute is an abstract method"
def execute(sql, name = nil, skip_logging = false)
end
undef_method :execute
# Returns the last auto-generated ID from the affected table.
def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
@ -53,36 +53,124 @@ module ActiveRecord
def delete(sql, name = nil)
delete_sql(sql, name)
end
# Checks whether there is currently no transaction active. This is done
# by querying the database driver, and does not use the transaction
# house-keeping information recorded by #increment_open_transactions and
# friends.
#
# Returns true if there is no transaction active, false if there is a
# transaction active, and nil if this information is unknown.
#
# Not all adapters supports transaction state introspection. Currently,
# only the PostgreSQL adapter supports this.
def outside_transaction?
nil
end
# Runs the given block in a database transaction, and returns the result
# of the block.
#
# == Nested transactions support
#
# Most databases don't support true nested transactions. At the time of
# writing, the only database that supports true nested transactions that
# we're aware of, is MS-SQL.
#
# 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
# Savepoints are supported by MySQL and PostgreSQL, but not SQLite3.
#
# It is safe to call this method if a database transaction is already open,
# i.e. if #transaction is called within another #transaction block. In case
# of a nested call, #transaction will behave as follows:
#
# - The block will be run without doing anything. All database statements
# that happen within the block are effectively appended to the already
# open database transaction.
# - However, if +:requires_new+ is set, the block will be wrapped in a
# database savepoint acting as a sub-transaction.
#
# === Caveats
#
# MySQL doesn't support DDL transactions. If you perform a DDL operation,
# then any created savepoints will be automatically released. For example,
# if you've created a savepoint, then you execute a CREATE TABLE statement,
# then the savepoint that was created will be automatically released.
#
# This means that, on MySQL, you shouldn't execute DDL operations inside
# a #transaction call that you know might create a savepoint. Otherwise,
# #transaction will raise exceptions when it tries to release the
# already-automatically-released savepoints:
#
# Model.connection.transaction do # BEGIN
# Model.connection.transaction(:requires_new => true) do # CREATE SAVEPOINT active_record_1
# Model.connection.create_table(...)
# # active_record_1 now automatically released
# end # RELEASE SAVEPOINT active_record_1 <--- BOOM! database error!
# end
def transaction(options = {})
options.assert_valid_keys :requires_new, :joinable
last_transaction_joinable = @transaction_joinable
if options.has_key?(:joinable)
@transaction_joinable = options[:joinable]
else
@transaction_joinable = true
end
requires_new = options[:requires_new] || !last_transaction_joinable
# Wrap a block in a transaction. Returns result of block.
def transaction(start_db_transaction = true)
transaction_open = false
begin
if block_given?
if start_db_transaction
begin_db_transaction
if requires_new || open_transactions == 0
if open_transactions == 0
begin_db_transaction
elsif requires_new
create_savepoint
end
increment_open_transactions
transaction_open = true
end
yield
end
rescue Exception => database_transaction_rollback
if transaction_open
if transaction_open && !outside_transaction?
transaction_open = false
rollback_db_transaction
decrement_open_transactions
if open_transactions == 0
rollback_db_transaction
else
rollback_to_savepoint
end
end
raise unless database_transaction_rollback.is_a? ActiveRecord::Rollback
raise unless database_transaction_rollback.is_a?(ActiveRecord::Rollback)
end
ensure
if transaction_open
@transaction_joinable = last_transaction_joinable
if outside_transaction?
@open_transactions = 0
elsif transaction_open
decrement_open_transactions
begin
commit_db_transaction
if open_transactions == 0
commit_db_transaction
else
release_savepoint
end
rescue Exception => database_transaction_rollback
rollback_db_transaction
if open_transactions == 0
rollback_db_transaction
else
rollback_to_savepoint
end
raise
end
end
end
# Begins the transaction (and turns off auto-committing).
def begin_db_transaction() end
@ -163,8 +251,8 @@ module ActiveRecord
# Returns an array of record hashes with the column names as keys and
# column values as values.
def select(sql, name = nil)
raise NotImplementedError, "select is an abstract method"
end
undef_method :select
# Returns the last auto-generated ID from the affected table.
def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)

View file

@ -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

View file

@ -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

View file

@ -3,6 +3,7 @@ require 'date'
require 'bigdecimal'
require 'bigdecimal/util'
# TODO: Autoload these files
require 'active_record/connection_adapters/abstract/schema_definitions'
require 'active_record/connection_adapters/abstract/schema_statements'
require 'active_record/connection_adapters/abstract/database_statements'
@ -65,6 +66,12 @@ module ActiveRecord
def supports_ddl_transactions?
false
end
# Does this adapter support savepoints? PostgreSQL and MySQL do, SQLite
# does not.
def supports_savepoints?
false
end
# Should primary key values be selected from their corresponding
# sequence before the insert statement? If true, next_sequence_value
@ -159,9 +166,26 @@ module ActiveRecord
@open_transactions -= 1
end
def log_info(sql, name, seconds)
def transaction_joinable=(joinable)
@transaction_joinable = joinable
end
def create_savepoint
end
def rollback_to_savepoint
end
def release_savepoint
end
def current_savepoint_name
"active_record_#{open_transactions}"
end
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
@ -170,9 +194,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)

View file

@ -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
@ -150,19 +152,23 @@ module ActiveRecord
# * <tt>:password</tt> - Defaults to nothing.
# * <tt>:database</tt> - The name of the database. No default, must be provided.
# * <tt>:encoding</tt> - (Optional) Sets the client encoding by executing "SET NAMES <encoding>" after connection.
# * <tt>:reconnect</tt> - Defaults to false (See MySQL documentation: http://dev.mysql.com/doc/refman/5.0/en/auto-reconnect.html).
# * <tt>:sslca</tt> - Necessary to use MySQL with an SSL connection.
# * <tt>:sslkey</tt> - Necessary to use MySQL with an SSL connection.
# * <tt>:sslcert</tt> - Necessary to use MySQL with an SSL connection.
# * <tt>:sslcapath</tt> - Necessary to use MySQL with an SSL connection.
# * <tt>:sslcipher</tt> - Necessary to use MySQL with an SSL connection.
#
# By default, the MysqlAdapter will consider all columns of type <tt>tinyint(1)</tt>
# as boolean. If you wish to disable this emulation (which was the default
# behavior in versions 0.13.1 and earlier) you can add the following line
# to your environment.rb file:
#
# ActiveRecord::ConnectionAdapters::MysqlAdapter.emulate_booleans = false
class MysqlAdapter < AbstractAdapter
##
# :singleton-method:
# By default, the MysqlAdapter will consider all columns of type <tt>tinyint(1)</tt>
# as boolean. If you wish to disable this emulation (which was the default
# behavior in versions 0.13.1 and earlier) you can add the following line
# to your environment.rb file:
#
# ActiveRecord::ConnectionAdapters::MysqlAdapter.emulate_booleans = false
cattr_accessor :emulate_booleans
self.emulate_booleans = true
@ -205,6 +211,10 @@ module ActiveRecord
def supports_migrations? #:nodoc:
true
end
def supports_savepoints? #:nodoc:
true
end
def native_database_types #:nodoc:
NATIVE_DATABASE_TYPES
@ -305,6 +315,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
@ -343,6 +354,17 @@ module ActiveRecord
# Transactions aren't supported
end
def create_savepoint
execute("SAVEPOINT #{current_savepoint_name}")
end
def rollback_to_savepoint
execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}")
end
def release_savepoint
execute("RELEASE SAVEPOINT #{current_savepoint_name}")
end
def add_limit_offset!(sql, options) #:nodoc:
if limit = options[:limit]
@ -411,7 +433,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
@ -422,7 +446,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]
@ -431,13 +456,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
@ -518,9 +546,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
@ -534,8 +564,6 @@ module ActiveRecord
private
def connect
@connection.reconnect = true if @connection.respond_to?(:reconnect=)
encoding = @config[:encoding]
if encoding
@connection.options(Mysql::SET_CHARSET_NAME, encoding) rescue nil
@ -546,6 +574,10 @@ module ActiveRecord
end
@connection.real_connect(*@connection_options)
# reconnect must be set after real_connect is called, because real_connect sets it to false internally
@connection.reconnect = !!@config[:reconnect] if @connection.respond_to?(:reconnect=)
configure_connection
end

View file

@ -272,6 +272,10 @@ module ActiveRecord
def supports_ddl_transactions?
true
end
def supports_savepoints?
true
end
# Returns the configured supported identifier length supported by PostgreSQL,
# or report the default of 63 on PostgreSQL 7.x.
@ -528,45 +532,26 @@ module ActiveRecord
def rollback_db_transaction
execute "ROLLBACK"
end
# ruby-pg defines Ruby constants for transaction status,
# ruby-postgres does not.
PQTRANS_IDLE = defined?(PGconn::PQTRANS_IDLE) ? PGconn::PQTRANS_IDLE : 0
# Check whether a transaction is active.
def transaction_active?
@connection.transaction_status != PQTRANS_IDLE
end
# Wrap a block in a transaction. Returns result of block.
def transaction(start_db_transaction = true)
transaction_open = false
begin
if block_given?
if start_db_transaction
begin_db_transaction
transaction_open = true
end
yield
end
rescue Exception => database_transaction_rollback
if transaction_open && transaction_active?
transaction_open = false
rollback_db_transaction
end
raise unless database_transaction_rollback.is_a? ActiveRecord::Rollback
end
ensure
if transaction_open && transaction_active?
begin
commit_db_transaction
rescue Exception => database_transaction_rollback
rollback_db_transaction
raise
end
if defined?(PGconn::PQTRANS_IDLE)
# The ruby-pg driver supports inspecting the transaction status,
# while the ruby-postgres driver does not.
def outside_transaction?
@connection.transaction_status == PGconn::PQTRANS_IDLE
end
end
def create_savepoint
execute("SAVEPOINT #{current_savepoint_name}")
end
def rollback_to_savepoint
execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}")
end
def release_savepoint
execute("RELEASE SAVEPOINT #{current_savepoint_name}")
end
# SCHEMA STATEMENTS ========================================
@ -950,13 +935,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

View file

@ -306,7 +306,7 @@ module ActiveRecord
end
def copy_table(from, to, options = {}) #:nodoc:
options = options.merge(:id => !columns(from).detect{|c| c.name == 'id'}.nil?)
options = options.merge(:id => (!columns(from).detect{|c| c.name == 'id'}.nil? && 'id' == primary_key(from).to_s))
create_table(to, options) do |definition|
@definition = definition
columns(from).each do |column|
@ -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

View file

@ -151,12 +151,12 @@ module ActiveRecord
def field_changed?(attr, old, value)
if column = column_for_attribute(attr)
if column.type == :integer && column.null && (old.nil? || old == 0)
# For nullable integer columns, NULL gets stored in database for blank (i.e. '') values.
if column.number? && column.null && (old.nil? || old == 0) && value.blank?
# For nullable numeric columns, NULL gets stored in database for blank (i.e. '') values.
# Hence we don't record it as a change if the value changes from nil to ''.
# If an old value of 0 is set to '' we want this to get changed to nil as otherwise it'll
# be typecast back to 0 (''.to_i => 0)
value = nil if value.blank?
value = nil
else
value = column.type_cast(value)
end
@ -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

View file

@ -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

View file

@ -1,6 +1,7 @@
require 'erb'
require 'yaml'
require 'csv'
require 'active_support/dependencies'
require 'active_support/test_case'
if RUBY_VERSION < '1.9'
@ -515,7 +516,7 @@ class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash)
all_loaded_fixtures.update(fixtures_map)
connection.transaction(connection.open_transactions.zero?) do
connection.transaction(:requires_new => true) do
fixtures.reverse.each { |fixture| fixture.delete_existing_fixtures }
fixtures.each { |fixture| fixture.insert_fixtures }
@ -813,186 +814,191 @@ class Fixture #:nodoc:
end
end
module Test #:nodoc:
module Unit #:nodoc:
class TestCase #:nodoc:
setup :setup_fixtures
teardown :teardown_fixtures
module ActiveRecord
module TestFixtures
def self.included(base)
base.class_eval do
setup :setup_fixtures
teardown :teardown_fixtures
superclass_delegating_accessor :fixture_path
superclass_delegating_accessor :fixture_table_names
superclass_delegating_accessor :fixture_class_names
superclass_delegating_accessor :use_transactional_fixtures
superclass_delegating_accessor :use_instantiated_fixtures # true, false, or :no_instances
superclass_delegating_accessor :pre_loaded_fixtures
superclass_delegating_accessor :fixture_path
superclass_delegating_accessor :fixture_table_names
superclass_delegating_accessor :fixture_class_names
superclass_delegating_accessor :use_transactional_fixtures
superclass_delegating_accessor :use_instantiated_fixtures # true, false, or :no_instances
superclass_delegating_accessor :pre_loaded_fixtures
self.fixture_table_names = []
self.use_transactional_fixtures = false
self.use_instantiated_fixtures = true
self.pre_loaded_fixtures = false
self.fixture_table_names = []
self.use_transactional_fixtures = false
self.use_instantiated_fixtures = true
self.pre_loaded_fixtures = false
@@already_loaded_fixtures = {}
self.fixture_class_names = {}
class << self
def set_fixture_class(class_names = {})
self.fixture_class_names = self.fixture_class_names.merge(class_names)
end
def fixtures(*table_names)
if table_names.first == :all
table_names = Dir["#{fixture_path}/*.yml"] + Dir["#{fixture_path}/*.csv"]
table_names.map! { |f| File.basename(f).split('.')[0..-2].join('.') }
else
table_names = table_names.flatten.map { |n| n.to_s }
end
self.fixture_table_names |= table_names
require_fixture_classes(table_names)
setup_fixture_accessors(table_names)
end
def try_to_load_dependency(file_name)
require_dependency file_name
rescue LoadError => e
# Let's hope the developer has included it himself
# Let's warn in case this is a subdependency, otherwise
# subdependency error messages are totally cryptic
if ActiveRecord::Base.logger
ActiveRecord::Base.logger.warn("Unable to load #{file_name}, underlying cause #{e.message} \n\n #{e.backtrace.join("\n")}")
end
end
def require_fixture_classes(table_names = nil)
(table_names || fixture_table_names).each do |table_name|
file_name = table_name.to_s
file_name = file_name.singularize if ActiveRecord::Base.pluralize_table_names
try_to_load_dependency(file_name)
end
end
def setup_fixture_accessors(table_names = nil)
table_names = [table_names] if table_names && !table_names.respond_to?(:each)
(table_names || fixture_table_names).each do |table_name|
table_name = table_name.to_s.tr('.', '_')
define_method(table_name) do |*fixtures|
force_reload = fixtures.pop if fixtures.last == true || fixtures.last == :reload
@fixture_cache[table_name] ||= {}
instances = fixtures.map do |fixture|
@fixture_cache[table_name].delete(fixture) if force_reload
if @loaded_fixtures[table_name][fixture.to_s]
@fixture_cache[table_name][fixture] ||= @loaded_fixtures[table_name][fixture.to_s].find
else
raise StandardError, "No fixture with name '#{fixture}' found for table '#{table_name}'"
end
end
instances.size == 1 ? instances.first : instances
end
end
end
def uses_transaction(*methods)
@uses_transaction = [] unless defined?(@uses_transaction)
@uses_transaction.concat methods.map(&:to_s)
end
def uses_transaction?(method)
@uses_transaction = [] unless defined?(@uses_transaction)
@uses_transaction.include?(method.to_s)
end
self.fixture_class_names = {}
end
def use_transactional_fixtures?
use_transactional_fixtures &&
!self.class.uses_transaction?(method_name)
end
def setup_fixtures
return unless defined?(ActiveRecord) && !ActiveRecord::Base.configurations.blank?
if pre_loaded_fixtures && !use_transactional_fixtures
raise RuntimeError, 'pre_loaded_fixtures requires use_transactional_fixtures'
end
@fixture_cache = {}
# Load fixtures once and begin transaction.
if use_transactional_fixtures?
if @@already_loaded_fixtures[self.class]
@loaded_fixtures = @@already_loaded_fixtures[self.class]
else
load_fixtures
@@already_loaded_fixtures[self.class] = @loaded_fixtures
end
ActiveRecord::Base.connection.increment_open_transactions
ActiveRecord::Base.connection.begin_db_transaction
# Load fixtures for every test.
else
Fixtures.reset_cache
@@already_loaded_fixtures[self.class] = nil
load_fixtures
end
# Instantiate fixtures for every test if requested.
instantiate_fixtures if use_instantiated_fixtures
end
def teardown_fixtures
return unless defined?(ActiveRecord) && !ActiveRecord::Base.configurations.blank?
unless use_transactional_fixtures?
Fixtures.reset_cache
end
# Rollback changes if a transaction is active.
if use_transactional_fixtures? && ActiveRecord::Base.connection.open_transactions != 0
ActiveRecord::Base.connection.rollback_db_transaction
ActiveRecord::Base.connection.decrement_open_transactions
end
ActiveRecord::Base.clear_active_connections!
end
private
def load_fixtures
@loaded_fixtures = {}
fixtures = Fixtures.create_fixtures(fixture_path, fixture_table_names, fixture_class_names)
unless fixtures.nil?
if fixtures.instance_of?(Fixtures)
@loaded_fixtures[fixtures.name] = fixtures
else
fixtures.each { |f| @loaded_fixtures[f.name] = f }
end
end
end
# for pre_loaded_fixtures, only require the classes once. huge speed improvement
@@required_fixture_classes = false
def instantiate_fixtures
if pre_loaded_fixtures
raise RuntimeError, 'Load fixtures before instantiating them.' if Fixtures.all_loaded_fixtures.empty?
unless @@required_fixture_classes
self.class.require_fixture_classes Fixtures.all_loaded_fixtures.keys
@@required_fixture_classes = true
end
Fixtures.instantiate_all_loaded_fixtures(self, load_instances?)
else
raise RuntimeError, 'Load fixtures before instantiating them.' if @loaded_fixtures.nil?
@loaded_fixtures.each do |table_name, fixtures|
Fixtures.instantiate_fixtures(self, table_name, fixtures, load_instances?)
end
end
end
def load_instances?
use_instantiated_fixtures != :no_instances
end
base.extend ClassMethods
end
module ClassMethods
def set_fixture_class(class_names = {})
self.fixture_class_names = self.fixture_class_names.merge(class_names)
end
def fixtures(*table_names)
if table_names.first == :all
table_names = Dir["#{fixture_path}/*.yml"] + Dir["#{fixture_path}/*.csv"]
table_names.map! { |f| File.basename(f).split('.')[0..-2].join('.') }
else
table_names = table_names.flatten.map { |n| n.to_s }
end
self.fixture_table_names |= table_names
require_fixture_classes(table_names)
setup_fixture_accessors(table_names)
end
def try_to_load_dependency(file_name)
require_dependency file_name
rescue LoadError => e
# Let's hope the developer has included it himself
# Let's warn in case this is a subdependency, otherwise
# subdependency error messages are totally cryptic
if ActiveRecord::Base.logger
ActiveRecord::Base.logger.warn("Unable to load #{file_name}, underlying cause #{e.message} \n\n #{e.backtrace.join("\n")}")
end
end
def require_fixture_classes(table_names = nil)
(table_names || fixture_table_names).each do |table_name|
file_name = table_name.to_s
file_name = file_name.singularize if ActiveRecord::Base.pluralize_table_names
try_to_load_dependency(file_name)
end
end
def setup_fixture_accessors(table_names = nil)
table_names = [table_names] if table_names && !table_names.respond_to?(:each)
(table_names || fixture_table_names).each do |table_name|
table_name = table_name.to_s.tr('.', '_')
define_method(table_name) do |*fixtures|
force_reload = fixtures.pop if fixtures.last == true || fixtures.last == :reload
@fixture_cache[table_name] ||= {}
instances = fixtures.map do |fixture|
@fixture_cache[table_name].delete(fixture) if force_reload
if @loaded_fixtures[table_name][fixture.to_s]
@fixture_cache[table_name][fixture] ||= @loaded_fixtures[table_name][fixture.to_s].find
else
raise StandardError, "No fixture with name '#{fixture}' found for table '#{table_name}'"
end
end
instances.size == 1 ? instances.first : instances
end
end
end
def uses_transaction(*methods)
@uses_transaction = [] unless defined?(@uses_transaction)
@uses_transaction.concat methods.map(&:to_s)
end
def uses_transaction?(method)
@uses_transaction = [] unless defined?(@uses_transaction)
@uses_transaction.include?(method.to_s)
end
end
def run_in_transaction?
use_transactional_fixtures &&
!self.class.uses_transaction?(method_name)
end
def setup_fixtures
return unless defined?(ActiveRecord) && !ActiveRecord::Base.configurations.blank?
if pre_loaded_fixtures && !use_transactional_fixtures
raise RuntimeError, 'pre_loaded_fixtures requires use_transactional_fixtures'
end
@fixture_cache = {}
@@already_loaded_fixtures ||= {}
# Load fixtures once and begin transaction.
if run_in_transaction?
if @@already_loaded_fixtures[self.class]
@loaded_fixtures = @@already_loaded_fixtures[self.class]
else
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
# Load fixtures for every test.
else
Fixtures.reset_cache
@@already_loaded_fixtures[self.class] = nil
load_fixtures
end
# Instantiate fixtures for every test if requested.
instantiate_fixtures if use_instantiated_fixtures
end
def teardown_fixtures
return unless defined?(ActiveRecord) && !ActiveRecord::Base.configurations.blank?
unless run_in_transaction?
Fixtures.reset_cache
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
end
ActiveRecord::Base.clear_active_connections!
end
private
def load_fixtures
@loaded_fixtures = {}
fixtures = Fixtures.create_fixtures(fixture_path, fixture_table_names, fixture_class_names)
unless fixtures.nil?
if fixtures.instance_of?(Fixtures)
@loaded_fixtures[fixtures.name] = fixtures
else
fixtures.each { |f| @loaded_fixtures[f.name] = f }
end
end
end
# for pre_loaded_fixtures, only require the classes once. huge speed improvement
@@required_fixture_classes = false
def instantiate_fixtures
if pre_loaded_fixtures
raise RuntimeError, 'Load fixtures before instantiating them.' if Fixtures.all_loaded_fixtures.empty?
unless @@required_fixture_classes
self.class.require_fixture_classes Fixtures.all_loaded_fixtures.keys
@@required_fixture_classes = true
end
Fixtures.instantiate_all_loaded_fixtures(self, load_instances?)
else
raise RuntimeError, 'Load fixtures before instantiating them.' if @loaded_fixtures.nil?
@loaded_fixtures.each do |table_name, fixtures|
Fixtures.instantiate_fixtures(self, table_name, fixtures, load_instances?)
end
end
end
def load_instances?
use_instantiated_fixtures != :no_instances
end
end
end

View file

@ -37,7 +37,7 @@ en:
# blank: "This is a custom blank message for User login"
# Will define custom blank validation message for User model and
# custom blank validation message for login attribute of User model.
models:
#models:
# Translate model names. Used in Model.human_name().
#models:

View file

@ -130,7 +130,9 @@ module ActiveRecord
# To run migrations against the currently configured database, use
# <tt>rake db:migrate</tt>. This will update the database by running all of the
# pending migrations, creating the <tt>schema_migrations</tt> table
# (see "About the schema_migrations table" section below) if missing.
# (see "About the schema_migrations table" section below) if missing. It will also
# invoke the db:schema:dump task, which will update your db/schema.rb file
# to match the structure of your database.
#
# To roll the database back to a previous migration version, use
# <tt>rake db:migrate VERSION=X</tt> where <tt>X</tt> is the version to which

View file

@ -39,7 +39,7 @@ module ActiveRecord
# 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 descendent upon which the \scopes were defined. But they are also available to
# 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

View file

@ -0,0 +1,279 @@
module ActiveRecord
module NestedAttributes #:nodoc:
def self.included(base)
base.extend(ClassMethods)
base.class_inheritable_accessor :reject_new_nested_attributes_procs, :instance_writer => false
base.reject_new_nested_attributes_procs = {}
end
# == Nested Attributes
#
# Nested attributes allow you to save attributes on associated records
# through the parent. By default nested attribute updating is turned off,
# you can enable it using the accepts_nested_attributes_for class method.
# When you enable nested attributes an attribute writer is defined on
# the model.
#
# The attribute writer is named after the association, which means that
# in the following example, two new methods are added to your model:
# <tt>author_attributes=(attributes)</tt> and
# <tt>pages_attributes=(attributes)</tt>.
#
# class Book < ActiveRecord::Base
# has_one :author
# has_many :pages
#
# accepts_nested_attributes_for :author, :pages
# end
#
# Note that the <tt>:autosave</tt> option is automatically enabled on every
# association that accepts_nested_attributes_for is used for.
#
# === One-to-one
#
# Consider a Member model that has one Avatar:
#
# class Member < ActiveRecord::Base
# has_one :avatar
# accepts_nested_attributes_for :avatar
# end
#
# Enabling nested attributes on a one-to-one association allows you to
# create the member and avatar in one go:
#
# params = { 'member' => { 'name' => 'Jack', 'avatar_attributes' => { 'icon' => 'smiling' } } }
# member = Member.create(params)
# member.avatar.icon #=> 'smiling'
#
# It also allows you to update the avatar through the member:
#
# params = { 'member' => { 'avatar_attributes' => { 'icon' => 'sad' } } }
# member.update_attributes params['member']
# member.avatar.icon #=> 'sad'
#
# By default you will only be able to set and update attributes on the
# associated model. If you want to destroy the associated model through the
# attributes hash, you have to enable it first using the
# <tt>:allow_destroy</tt> option.
#
# class Member < ActiveRecord::Base
# has_one :avatar
# accepts_nested_attributes_for :avatar, :allow_destroy => true
# end
#
# Now, when you add the <tt>_delete</tt> key to the attributes hash, with a
# value that evaluates to +true+, you will destroy the associated model:
#
# member.avatar_attributes = { '_delete' => '1' }
# member.avatar.marked_for_destruction? # => true
# member.save
# member.avatar #=> nil
#
# Note that the model will _not_ be destroyed until the parent is saved.
#
# === One-to-many
#
# Consider a member that has a number of posts:
#
# class Member < ActiveRecord::Base
# has_many :posts
# accepts_nested_attributes_for :posts, :reject_if => proc { |attributes| attributes['title'].blank? }
# end
#
# You can now set or update attributes on an associated post model through
# the attribute hash.
#
# For each key in the hash that starts with the string 'new' a new model
# will be instantiated. When the proc given with the <tt>:reject_if</tt>
# option evaluates to +false+ for a certain attribute hash no record will
# be built for that hash.
#
# params = { 'member' => {
# 'name' => 'joe', 'posts_attributes' => {
# 'new_12345' => { 'title' => 'Kari, the awesome Ruby documentation browser!' },
# 'new_54321' => { 'title' => 'The egalitarian assumption of the modern citizen' },
# 'new_67890' => { 'title' => '' } # This one matches the :reject_if proc and will not be instantiated.
# }
# }}
#
# member = Member.create(params['member'])
# member.posts.length #=> 2
# member.posts.first.title #=> 'Kari, the awesome Ruby documentation browser!'
# member.posts.second.title #=> 'The egalitarian assumption of the modern citizen'
#
# When the key for post attributes is an integer, the associated post with
# that ID will be updated:
#
# member.attributes = {
# 'name' => 'Joe',
# 'posts_attributes' => {
# '1' => { 'title' => '[UPDATED] An, as of yet, undisclosed awesome Ruby documentation browser!' },
# '2' => { 'title' => '[UPDATED] other post' }
# }
# }
#
# By default the associated models are protected from being destroyed. If
# you want to destroy any of the associated models through the attributes
# hash, you have to enable it first using the <tt>:allow_destroy</tt>
# option.
#
# This will allow you to specify which models to destroy in the attributes
# hash by setting the '_delete' attribute to a value that evaluates to
# +true+:
#
# class Member < ActiveRecord::Base
# has_many :posts
# accepts_nested_attributes_for :posts, :allow_destroy => true
# end
#
# params = {'member' => { 'name' => 'joe', 'posts_attributes' => {
# '2' => { '_delete' => '1' }
# }}}
# member.attributes = params['member']
# member.posts.detect { |p| p.id == 2 }.marked_for_destruction? # => true
# member.posts.length #=> 2
# member.save
# member.posts.length # => 1
#
# === Saving
#
# All changes to models, including the destruction of those marked for
# destruction, are saved and destroyed automatically and atomically when
# the parent model is saved. This happens inside the transaction initiated
# by the parents save method. See ActiveRecord::AutosaveAssociation.
module ClassMethods
# Defines an attributes writer for the specified association(s).
#
# Supported options:
# [:allow_destroy]
# If true, destroys any members from the attributes hash with a
# <tt>_delete</tt> key and a value that converts to +true+
# (eg. 1, '1', true, or 'true'). This option is off by default.
# [:reject_if]
# Allows you to specify a Proc that checks whether a record should be
# built for a certain attribute hash. The hash is passed to the Proc
# and the Proc should return either +true+ or +false+. When no Proc
# is specified a record will be built for all attribute hashes.
#
# Examples:
# accepts_nested_attributes_for :avatar
# accepts_nested_attributes_for :avatar, :allow_destroy => true
# accepts_nested_attributes_for :avatar, :reject_if => proc { ... }
# accepts_nested_attributes_for :avatar, :posts, :allow_destroy => true, :reject_if => proc { ... }
def accepts_nested_attributes_for(*attr_names)
options = { :allow_destroy => false }
options.update(attr_names.extract_options!)
options.assert_valid_keys(:allow_destroy, :reject_if)
attr_names.each do |association_name|
if reflection = reflect_on_association(association_name)
type = case reflection.macro
when :has_one, :belongs_to
:one_to_one
when :has_many, :has_and_belongs_to_many
:collection
end
reflection.options[:autosave] = true
self.reject_new_nested_attributes_procs[association_name.to_sym] = options[:reject_if]
# def pirate_attributes=(attributes)
# assign_nested_attributes_for_one_to_one_association(:pirate, attributes, false)
# end
class_eval %{
def #{association_name}_attributes=(attributes)
assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes, #{options[:allow_destroy]})
end
}, __FILE__, __LINE__
else
raise ArgumentError, "No association found for name `#{association_name}'. Has it been defined yet?"
end
end
end
end
# Returns ActiveRecord::AutosaveAssociation::marked_for_destruction?
# It's used in conjunction with fields_for to build a form element
# for the destruction of this association.
#
# See ActionView::Helpers::FormHelper::fields_for for more info.
def _delete
marked_for_destruction?
end
private
# Assigns the given attributes to the association. An association will be
# build if it doesn't exist yet.
def assign_nested_attributes_for_one_to_one_association(association_name, attributes, allow_destroy)
if should_destroy_nested_attributes_record?(allow_destroy, attributes)
send(association_name).mark_for_destruction
else
(send(association_name) || send("build_#{association_name}")).attributes = attributes
end
end
# Assigns the given attributes to the collection association.
#
# Keys containing an ID for an associated record will update that record.
# Keys starting with <tt>new</tt> will instantiate a new record for that
# association.
#
# For example:
#
# assign_nested_attributes_for_collection_association(:people, {
# '1' => { 'name' => 'Peter' },
# 'new_43' => { 'name' => 'John' }
# })
#
# Will update the name of the Person with ID 1 and create a new associated
# person with the name 'John'.
def assign_nested_attributes_for_collection_association(association_name, attributes, allow_destroy)
unless attributes.is_a?(Hash)
raise ArgumentError, "Hash expected, got #{attributes.class.name} (#{attributes.inspect})"
end
# Make sure any new records sorted by their id before they're build.
sorted_by_id = attributes.sort_by { |id, _| id.is_a?(String) ? id.sub(/^new_/, '').to_i : id }
sorted_by_id.each do |id, record_attributes|
if id.acts_like?(:string) && id.starts_with?('new_')
build_new_nested_attributes_record(association_name, record_attributes)
else
assign_to_or_destroy_nested_attributes_record(association_name, id, record_attributes, allow_destroy)
end
end
end
# Returns +true+ if <tt>allow_destroy</tt> is enabled and the attributes
# contains a truthy value for the key <tt>'_delete'</tt>.
#
# It will _always_ remove the <tt>'_delete'</tt> key, if present.
def should_destroy_nested_attributes_record?(allow_destroy, attributes)
ConnectionAdapters::Column.value_to_boolean(attributes.delete('_delete')) && allow_destroy
end
# Builds a new record with the given attributes.
#
# If a <tt>:reject_if</tt> proc exists for this association, it will be
# called with the attributes as its argument. If the proc returns a truthy
# value, the record is _not_ build.
def build_new_nested_attributes_record(association_name, attributes)
if reject_proc = self.class.reject_new_nested_attributes_procs[association_name]
return if reject_proc.call(attributes)
end
send(association_name).build(attributes)
end
# Assigns the attributes to the record specified by +id+. Or marks it for
# destruction if #should_destroy_nested_attributes_record? returns +true+.
def assign_to_or_destroy_nested_attributes_record(association_name, id, attributes, allow_destroy)
record = send(association_name).detect { |record| record.id == id.to_i }
if should_destroy_nested_attributes_record?(allow_destroy, attributes)
record.mark_for_destruction
else
record.attributes = attributes
end
end
end
end

View file

@ -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
# 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 initialize(app)
@app = app
end
def call(env)
ActiveRecord::Base.cache do
@app.call(env)
end
end
end

View file

@ -65,6 +65,11 @@ module ActiveRecord
def reflect_on_association(association)
reflections[association].is_a?(AssociationReflection) ? reflections[association] : nil
end
# Returns an array of AssociationReflection objects for all associations which have <tt>:autosave</tt> enabled.
def reflect_on_all_autosave_associations
reflections.values.select { |reflection| reflection.options[:autosave] }
end
end
@ -198,6 +203,14 @@ module ActiveRecord
end
end
def columns(tbl_name, log_msg)
@columns ||= klass.connection.columns(tbl_name, log_msg)
end
def reset_column_information
@columns = nil
end
def check_validity!
end

View file

@ -7,6 +7,8 @@ module ActiveRecord
class SchemaDumper #:nodoc:
private_class_method :new
##
# :singleton-method:
# A list of tables which should not be dumped to the schema.
# Acceptable values are strings as well as regexp.
# This setting is only used if ActiveRecord::Base.schema_format == :ruby

View file

@ -1,3 +1,5 @@
require 'active_support/json'
module ActiveRecord #:nodoc:
module Serialization
class Serializer #:nodoc:
@ -95,4 +97,4 @@ module ActiveRecord #:nodoc:
end
require 'active_record/serializers/xml_serializer'
require 'active_record/serializers/json_serializer'
require 'active_record/serializers/json_serializer'

View file

@ -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

View file

@ -0,0 +1,314 @@
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
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

View file

@ -1,15 +1,7 @@
require "active_support/test_case"
module ActiveRecord
module ActiveRecord
class TestCase < ActiveSupport::TestCase #:nodoc:
self.fixture_path = FIXTURES_ROOT
self.use_instantiated_fixtures = false
self.use_transactional_fixtures = true
def create_fixtures(*table_names, &block)
Fixtures.create_fixtures(FIXTURES_ROOT, table_names, {}, &block)
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
@ -35,6 +27,7 @@ module ActiveRecord
$queries_executed = []
yield
ensure
%w{ BEGIN COMMIT }.each { |x| $queries_executed.delete(x) }
assert_equal num, $queries_executed.size, "#{$queries_executed.size} instead of #{num} queries were executed.#{$queries_executed.size == 0 ? '' : "\nQueries:\n#{$queries_executed.join("\n")}"}"
end

View file

@ -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

View file

@ -120,16 +120,66 @@ module ActiveRecord
# end
#
# One should restart the entire transaction if a StatementError occurred.
#
# == Nested transactions
#
# #transaction calls can be nested. By default, this makes all database
# statements in the nested transaction block become part of the parent
# transaction. For example:
#
# User.transaction do
# User.create(:username => 'Kotori')
# User.transaction do
# User.create(:username => 'Nemu')
# raise ActiveRecord::Rollback
# end
# end
#
# User.find(:all) # => empty
#
# It is also possible to requires a sub-transaction by passing
# <tt>:requires_new => true</tt>. If anything goes wrong, the
# database rolls back to the beginning of the sub-transaction
# without rolling back the parent transaction. For example:
#
# User.transaction do
# User.create(:username => 'Kotori')
# User.transaction(:requires_new => true) do
# User.create(:username => 'Nemu')
# raise ActiveRecord::Rollback
# end
# end
#
# User.find(:all) # => Returns only Kotori
#
# Most databases don't support true nested transactions. At the time of
# writing, the only database that we're aware of that supports true nested
# transactions, is MS-SQL. Because of this, Active Record emulates nested
# transactions by using savepoints. See
# http://dev.mysql.com/doc/refman/5.0/en/savepoints.html
# for more information about savepoints.
#
# === Caveats
#
# If you're on MySQL, then do not use DDL operations in nested transactions
# blocks that are emulated with savepoints. That is, do not execute statements
# like 'CREATE TABLE' inside such blocks. This is because MySQL automatically
# releases all savepoints upon executing a DDL operation. When #transaction
# is finished and tries to release the savepoint it created earlier, a
# database error will occur because the savepoint has already been
# automatically released. The following example demonstrates the problem:
#
# Model.connection.transaction do # BEGIN
# Model.connection.transaction(:requires_new => true) do # CREATE SAVEPOINT active_record_1
# Model.connection.create_table(...) # active_record_1 now automatically released
# end # RELEASE savepoint active_record_1
# # ^^^^ BOOM! database error!
# end
module ClassMethods
# See ActiveRecord::Transactions::ClassMethods for detailed documentation.
def transaction(&block)
connection.increment_open_transactions
begin
connection.transaction(connection.open_transactions == 1, &block)
ensure
connection.decrement_open_transactions
end
def transaction(options = {}, &block)
# See the ConnectionAdapters::DatabaseStatements#transaction API docs.
connection.transaction(options, &block)
end
end
@ -147,7 +197,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.
@ -175,7 +225,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

View file

@ -203,9 +203,8 @@ 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 + ' ' + message
full_messages << attr_name + I18n.t('activerecord.errors.format.separator', :default => ' ') + message
end
end
end
@ -494,18 +493,20 @@ module ActiveRecord
# The first_name attribute must be in the object and it cannot be blank.
#
# If you want to validate the presence of a boolean field (where the real values are true and false),
# you will want to use validates_inclusion_of :field_name, :in => [true, false]
# This is due to the way Object#blank? handles boolean values. false.blank? # => true
# you will want to use <tt>validates_inclusion_of :field_name, :in => [true, false]</tt>.
#
# This is due to the way Object#blank? handles boolean values: <tt>false.blank? # => true</tt>.
#
# Configuration options:
# * <tt>message</tt> - A custom error message (default is: "can't be blank").
# * <tt>on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>).
# * <tt>on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>,
# <tt>:update</tt>).
# * <tt>if</tt> - Specifies a method, proc or string to call to determine if the validation should
# occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The
# method, proc or string should return or evaluate to a true or false value.
# occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>).
# The method, proc or string should return or evaluate to a true or false value.
# * <tt>unless</tt> - Specifies a method, proc or string to call to determine if the validation should
# not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }). The
# method, proc or string should return or evaluate to a true or false value.
# not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>).
# The method, proc or string should return or evaluate to a true or false value.
#
def validates_presence_of(*attr_names)
configuration = { :on => :save }
@ -574,6 +575,8 @@ module ActiveRecord
# Get range option and value.
option = range_options.first
option_value = options[range_options.first]
key = {:is => :wrong_length, :minimum => :too_short, :maximum => :too_long}[option]
custom_message = options[:message] || options[key]
case option
when :within, :in
@ -582,9 +585,9 @@ module ActiveRecord
validates_each(attrs, options) do |record, attr, value|
value = options[:tokenizer].call(value) if value.kind_of?(String)
if value.nil? or value.size < option_value.begin
record.errors.add(attr, :too_short, :default => options[:too_short], :count => option_value.begin)
record.errors.add(attr, :too_short, :default => custom_message || options[:too_short], :count => option_value.begin)
elsif value.size > option_value.end
record.errors.add(attr, :too_long, :default => options[:too_long], :count => option_value.end)
record.errors.add(attr, :too_long, :default => custom_message || options[:too_long], :count => option_value.end)
end
end
when :is, :minimum, :maximum
@ -592,13 +595,10 @@ module ActiveRecord
# Declare different validations per option.
validity_checks = { :is => "==", :minimum => ">=", :maximum => "<=" }
message_options = { :is => :wrong_length, :minimum => :too_short, :maximum => :too_long }
validates_each(attrs, options) do |record, attr, value|
value = options[:tokenizer].call(value) if value.kind_of?(String)
unless !value.nil? and value.size.method(validity_checks[option])[option_value]
key = message_options[option]
custom_message = options[:message] || options[key]
record.errors.add(attr, key, :default => custom_message, :count => option_value)
end
end
@ -903,7 +903,7 @@ module ActiveRecord
configuration.update(attr_names.extract_options!)
validates_each(attr_names, configuration) do |record, attr_name, value|
unless (value.is_a?(Array) ? value : [value]).inject(true) { |v, r| (r.nil? || r.valid?) && v }
unless (value.is_a?(Array) ? value : [value]).collect { |r| r.nil? || r.valid? }.all?
record.errors.add(attr_name, :invalid, :default => configuration[:message], :value => value)
end
end
@ -1047,15 +1047,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

View file

@ -1,8 +1,8 @@
module ActiveRecord
module VERSION #:nodoc:
MAJOR = 2
MINOR = 2
TINY = 2
MINOR = 3
TINY = 0
STRING = [MAJOR, MINOR, TINY].join('.')
end