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

File diff suppressed because it is too large Load diff

View file

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

View file

@ -32,9 +32,13 @@ task :default => :test
desc 'Run mysql, sqlite, and postgresql tests'
task :test => %w(test_mysql test_sqlite3 test_postgresql)
for adapter in %w( mysql postgresql sqlite sqlite3 firebird db2 oracle sybase openbase frontbase )
for adapter in %w( mysql postgresql sqlite sqlite3 firebird db2 oracle sybase openbase frontbase jdbcmysql jdbcpostgresql jdbcsqlite3 jdbcderby jdbch2 jdbchsqldb )
Rake::TestTask.new("test_#{adapter}") { |t|
t.libs << "test" << "test/connections/native_#{adapter}"
if adapter =~ /jdbc/
t.libs << "test" << "test/connections/jdbc_#{adapter}"
else
t.libs << "test" << "test/connections/native_#{adapter}"
end
adapter_short = adapter == 'db2' ? adapter : adapter[/^[a-z]+/]
t.test_files=Dir.glob( "test/cases/**/*_test{,_#{adapter_short}}.rb" ).sort
t.verbose = true
@ -171,7 +175,7 @@ spec = Gem::Specification.new do |s|
s.files = s.files + Dir.glob( "#{dir}/**/*" ).delete_if { |item| item.include?( "\.svn" ) }
end
s.add_dependency('activesupport', '= 2.2.2' + PKG_BUILD)
s.add_dependency('activesupport', '= 2.3.0' + PKG_BUILD)
s.files.delete FIXTURES_ROOT + "/fixture_database.sqlite"
s.files.delete FIXTURES_ROOT + "/fixture_database_2.sqlite"

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

View file

@ -1,5 +1,4 @@
require "cases/helper"
require 'active_record/schema'
if ActiveRecord::Base.connection.supports_migrations?

View file

@ -104,6 +104,14 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase
authors.first.posts.first.special_comments.first.post.very_special_comment
end
end
def test_eager_association_loading_where_first_level_returns_nil
authors = Author.find(:all, :include => {:post_about_thinking => :comments}, :order => 'authors.id DESC')
assert_equal [authors(:mary), authors(:david)], authors
assert_no_queries do
authors[1].post_about_thinking.comments.first
end
end
end
require 'models/vertex'

View file

@ -1,6 +1,7 @@
require "cases/helper"
require 'models/post'
require 'models/tagging'
require 'models/tag'
require 'models/comment'
require 'models/author'
require 'models/category'
@ -145,7 +146,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
def test_finding_with_includes_on_null_belongs_to_association_with_same_include_includes_only_once
post = posts(:welcome)
post.update_attributes!(:author => nil)
post = assert_queries(2) { Post.find(post.id, :include => {:author_with_address => :author_address}) } # find the post, then find the author which is null so no query for the address
post = assert_queries(1) { Post.find(post.id, :include => {:author_with_address => :author_address}) } # find the post, then find the author which is null so no query for the author or address
assert_no_queries do
assert_equal nil, post.author_with_address
end
@ -385,12 +386,28 @@ class EagerAssociationTest < ActiveRecord::TestCase
assert_equal count, posts.size
end
def test_eager_with_has_many_and_limit_ond_high_offset
def test_eager_with_has_many_and_limit_and_high_offset
posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :offset => 10, :conditions => [ "authors.name = ?", 'David' ])
assert_equal 0, posts.size
end
def test_count_eager_with_has_many_and_limit_ond_high_offset
def test_eager_with_has_many_and_limit_and_high_offset_and_multiple_array_conditions
assert_queries(1) do
posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :offset => 10,
:conditions => [ "authors.name = ? and comments.body = ?", 'David', 'go crazy' ])
assert_equal 0, posts.size
end
end
def test_eager_with_has_many_and_limit_and_high_offset_and_multiple_hash_conditions
assert_queries(1) do
posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :offset => 10,
:conditions => { 'authors.name' => 'David', 'comments.body' => 'go crazy' })
assert_equal 0, posts.size
end
end
def test_count_eager_with_has_many_and_limit_and_high_offset
posts = Post.count(:all, :include => [ :author, :comments ], :limit => 2, :offset => 10, :conditions => [ "authors.name = ?", 'David' ])
assert_equal 0, posts
end
@ -689,4 +706,117 @@ class EagerAssociationTest < ActiveRecord::TestCase
def test_order_on_join_table_with_include_and_limit
assert_equal 5, Developer.find(:all, :include => 'projects', :order => 'developers_projects.joined_on DESC', :limit => 5).size
end
def test_eager_loading_with_order_on_joined_table_preloads
posts = assert_queries(2) do
Post.find(:all, :joins => :comments, :include => :author, :order => 'comments.id DESC')
end
assert_equal posts(:eager_other), posts[0]
assert_equal authors(:mary), assert_no_queries { posts[0].author}
end
def test_eager_loading_with_conditions_on_joined_table_preloads
posts = assert_queries(2) do
Post.find(:all, :select => 'distinct posts.*', :include => :author, :joins => [:comments], :conditions => "comments.body like 'Thank you%'", :order => 'posts.id')
end
assert_equal [posts(:welcome)], posts
assert_equal authors(:david), assert_no_queries { posts[0].author}
posts = assert_queries(2) do
Post.find(:all, :select => 'distinct posts.*', :include => :author, :joins => [:comments], :conditions => "comments.body like 'Thank you%'", :order => 'posts.id')
end
assert_equal [posts(:welcome)], posts
assert_equal authors(:david), assert_no_queries { posts[0].author}
posts = assert_queries(2) do
Post.find(:all, :include => :author, :joins => {:taggings => :tag}, :conditions => "tags.name = 'General'", :order => 'posts.id')
end
assert_equal posts(:welcome, :thinking), posts
posts = assert_queries(2) do
Post.find(:all, :include => :author, :joins => {:taggings => {:tag => :taggings}}, :conditions => "taggings_tags.super_tag_id=2", :order => 'posts.id')
end
assert_equal posts(:welcome, :thinking), posts
end
def test_eager_loading_with_conditions_on_string_joined_table_preloads
posts = assert_queries(2) do
Post.find(:all, :select => 'distinct posts.*', :include => :author, :joins => "INNER JOIN comments on comments.post_id = posts.id", :conditions => "comments.body like 'Thank you%'", :order => 'posts.id')
end
assert_equal [posts(:welcome)], posts
assert_equal authors(:david), assert_no_queries { posts[0].author}
posts = assert_queries(2) do
Post.find(:all, :select => 'distinct posts.*', :include => :author, :joins => ["INNER JOIN comments on comments.post_id = posts.id"], :conditions => "comments.body like 'Thank you%'", :order => 'posts.id')
end
assert_equal [posts(:welcome)], posts
assert_equal authors(:david), assert_no_queries { posts[0].author}
end
def test_eager_loading_with_select_on_joined_table_preloads
posts = assert_queries(2) do
Post.find(:all, :select => 'posts.*, authors.name as author_name', :include => :comments, :joins => :author, :order => 'posts.id')
end
assert_equal 'David', posts[0].author_name
assert_equal posts(:welcome).comments, assert_no_queries { posts[0].comments}
end
def test_eager_loading_with_conditions_on_join_model_preloads
authors = assert_queries(2) do
Author.find(:all, :include => :author_address, :joins => :comments, :conditions => "posts.title like 'Welcome%'")
end
assert_equal authors(:david), authors[0]
assert_equal author_addresses(:david_address), authors[0].author_address
end
def test_preload_belongs_to_uses_exclusive_scope
people = Person.males.find(:all, :include => :primary_contact)
assert_not_equal people.length, 0
people.each do |person|
assert_no_queries {assert_not_nil person.primary_contact}
assert_equal Person.find(person.id).primary_contact, person.primary_contact
end
end
def test_preload_has_many_uses_exclusive_scope
people = Person.males.find :all, :include => :agents
people.each do |person|
assert_equal Person.find(person.id).agents, person.agents
end
end
def test_preload_has_many_using_primary_key
expected = Firm.find(:first).clients_using_primary_key.to_a
firm = Firm.find :first, :include => :clients_using_primary_key
assert_no_queries do
assert_equal expected, firm.clients_using_primary_key
end
end
def test_include_has_many_using_primary_key
expected = Firm.find(1).clients_using_primary_key.sort_by &:name
firm = Firm.find 1, :include => :clients_using_primary_key, :order => 'clients_using_primary_keys_companies.name'
assert_no_queries do
assert_equal expected, firm.clients_using_primary_key
end
end
def test_preload_has_one_using_primary_key
expected = Firm.find(:first).account_using_primary_key
firm = Firm.find :first, :include => :account_using_primary_key
assert_no_queries do
assert_equal expected, firm.account_using_primary_key
end
end
def test_include_has_one_using_primary_key
expected = Firm.find(1).account_using_primary_key
firm = Firm.find(:all, :include => :account_using_primary_key, :order => 'accounts.id').detect {|f| f.id == 1}
assert_no_queries do
assert_equal expected, firm.account_using_primary_key
end
end
end

View file

@ -658,6 +658,11 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_equal 1, categories(:technology).posts_gruoped_by_title.size
end
def test_find_scoped_grouped_having
assert_equal 2, projects(:active_record).well_payed_salary_groups.size
assert projects(:active_record).well_payed_salary_groups.all? { |g| g.salary > 10000 }
end
def test_get_ids
assert_equal projects(:active_record, :action_controller).map(&:id).sort, developers(:david).project_ids.sort
assert_equal [projects(:active_record).id], developers(:jamis).project_ids
@ -770,4 +775,15 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
end
end
end
def test_caching_of_columns
david = Developer.find(1)
# clear cache possibly created by other tests
david.projects.reset_column_information
assert_queries(0) { david.projects.columns; david.projects.columns }
# and again to verify that reset_column_information clears the cache correctly
david.projects.reset_column_information
assert_queries(0) { david.projects.columns; david.projects.columns }
end
end

View file

@ -255,6 +255,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal 2, companies(:first_firm).clients_grouped_by_name.length
end
def test_find_scoped_grouped_having
assert_equal 1, authors(:david).popular_grouped_posts.length
assert_equal 0, authors(:mary).popular_grouped_posts.length
end
def test_adding
force_signal37_to_load_all_clients_of_firm
natural = Client.new("name" => "Natural Company")
@ -660,6 +665,19 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal 1, Client.find_all_by_client_of(firm.id).size
end
def test_dependent_association_respects_optional_hash_conditions_on_delete
firm = companies(:odegy)
Client.create(:client_of => firm.id, :name => "BigShot Inc.")
Client.create(:client_of => firm.id, :name => "SmallTime Inc.")
# only one of two clients is included in the association due to the :conditions key
assert_equal 2, Client.find_all_by_client_of(firm.id).size
assert_equal 1, firm.dependent_sanitized_conditional_clients_of_firm.size
firm.destroy
# only the correctly associated client should have been deleted
assert_equal 1, Client.find_all_by_client_of(firm.id).size
end
def test_creation_respects_hash_condition
ms_client = companies(:first_firm).clients_like_ms_with_hash_conditions.build
@ -680,7 +698,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
authors(:david).destroy
end
assert_equal [author_address.id], AuthorAddress.destroyed_author_address_ids[authors(:david).id]
assert_equal nil, AuthorAddress.find_by_id(authors(:david).author_address_id)
assert_equal nil, AuthorAddress.find_by_id(authors(:david).author_address_extra_id)
end
def test_invalid_belongs_to_dependent_option_raises_exception
@ -1097,5 +1116,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert !client_association.respond_to?(:private_method)
assert client_association.respond_to?(:private_method, true)
end
def test_creating_using_primary_key
firm = Firm.find(:first)
client = firm.clients_using_primary_key.create!(:name => 'test')
assert_equal firm.name, client.firm_name
end
end

View file

@ -3,6 +3,9 @@ require 'models/post'
require 'models/person'
require 'models/reader'
require 'models/comment'
require 'models/tag'
require 'models/tagging'
require 'models/author'
class HasManyThroughAssociationsTest < ActiveRecord::TestCase
fixtures :posts, :readers, :people, :comments, :authors
@ -201,6 +204,10 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
assert_equal 2, people(:michael).posts.count(:include => :readers)
end
def test_inner_join_with_quoted_table_name
assert_equal 2, people(:michael).jobs.size
end
def test_get_ids
assert_equal [posts(:welcome).id, posts(:authorless).id].sort, people(:michael).post_ids.sort
end

View file

@ -1,5 +1,6 @@
require "cases/helper"
require 'models/club'
require 'models/member_type'
require 'models/member'
require 'models/membership'
require 'models/sponsor'
@ -7,7 +8,7 @@ require 'models/organization'
require 'models/member_detail'
class HasOneThroughAssociationsTest < ActiveRecord::TestCase
fixtures :members, :clubs, :memberships, :sponsors, :organizations
fixtures :member_types, :members, :clubs, :memberships, :sponsors, :organizations
def setup
@member = members(:groucho)
@ -158,4 +159,18 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
assert @new_organization.members.include?(@member)
end
def test_preloading_has_one_through_on_belongs_to
assert_not_nil @member.member_type
@organization = organizations(:nsa)
@member_detail = MemberDetail.new
@member.member_detail = @member_detail
@member.organization = @organization
@member_details = assert_queries(3) do
MemberDetail.find(:all, :include => :member_type)
end
@new_detail = @member_details[0]
assert @new_detail.loaded_member_type?
assert_not_nil assert_no_queries { @new_detail.member_type }
end
end

View file

@ -0,0 +1,386 @@
require "cases/helper"
require "models/pirate"
require "models/ship"
require "models/ship_part"
require "models/bird"
require "models/parrot"
require "models/treasure"
class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase
def test_autosave_should_be_a_valid_option_for_has_one
assert base.valid_keys_for_has_one_association.include?(:autosave)
end
def test_autosave_should_be_a_valid_option_for_belongs_to
assert base.valid_keys_for_belongs_to_association.include?(:autosave)
end
def test_autosave_should_be_a_valid_option_for_has_many
assert base.valid_keys_for_has_many_association.include?(:autosave)
end
def test_autosave_should_be_a_valid_option_for_has_and_belongs_to_many
assert base.valid_keys_for_has_and_belongs_to_many_association.include?(:autosave)
end
private
def base
ActiveRecord::Base
end
end
class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
self.use_transactional_fixtures = false
def setup
@pirate = Pirate.create(:catchphrase => "Don' botharrr talkin' like one, savvy?")
@ship = @pirate.create_ship(:name => 'Nights Dirty Lightning')
end
# reload
def test_a_marked_for_destruction_record_should_not_be_be_marked_after_reload
@pirate.mark_for_destruction
@pirate.ship.mark_for_destruction
assert !@pirate.reload.marked_for_destruction?
assert !@pirate.ship.marked_for_destruction?
end
# has_one
def test_should_destroy_a_child_association_as_part_of_the_save_transaction_if_it_was_marked_for_destroyal
assert !@pirate.ship.marked_for_destruction?
@pirate.ship.mark_for_destruction
id = @pirate.ship.id
assert @pirate.ship.marked_for_destruction?
assert Ship.find_by_id(id)
@pirate.save
assert_nil @pirate.reload.ship
assert_nil Ship.find_by_id(id)
end
def test_should_rollback_destructions_if_an_exception_occurred_while_saving_a_child
# Stub the save method of the @pirate.ship instance to destroy and then raise an exception
class << @pirate.ship
def save(*args)
super
destroy
raise 'Oh noes!'
end
end
assert_raise(RuntimeError) { assert !@pirate.save }
assert_not_nil @pirate.reload.ship
end
# belongs_to
def test_should_destroy_a_parent_association_as_part_of_the_save_transaction_if_it_was_marked_for_destroyal
assert !@ship.pirate.marked_for_destruction?
@ship.pirate.mark_for_destruction
id = @ship.pirate.id
assert @ship.pirate.marked_for_destruction?
assert Pirate.find_by_id(id)
@ship.save
assert_nil @ship.reload.pirate
assert_nil Pirate.find_by_id(id)
end
def test_should_rollback_destructions_if_an_exception_occurred_while_saving_a_parent
# Stub the save method of the @ship.pirate instance to destroy and then raise an exception
class << @ship.pirate
def save(*args)
super
destroy
raise 'Oh noes!'
end
end
assert_raise(RuntimeError) { assert !@ship.save }
assert_not_nil @ship.reload.pirate
end
# has_many & has_and_belongs_to
%w{ parrots birds }.each do |association_name|
define_method("test_should_destroy_#{association_name}_as_part_of_the_save_transaction_if_they_were_marked_for_destroyal") do
2.times { |i| @pirate.send(association_name).create!(:name => "#{association_name}_#{i}") }
assert !@pirate.send(association_name).any? { |child| child.marked_for_destruction? }
@pirate.send(association_name).each { |child| child.mark_for_destruction }
klass = @pirate.send(association_name).first.class
ids = @pirate.send(association_name).map(&:id)
assert @pirate.send(association_name).all? { |child| child.marked_for_destruction? }
ids.each { |id| assert klass.find_by_id(id) }
@pirate.save
assert @pirate.reload.send(association_name).empty?
ids.each { |id| assert_nil klass.find_by_id(id) }
end
define_method("test_should_rollback_destructions_if_an_exception_occurred_while_saving_#{association_name}") do
2.times { |i| @pirate.send(association_name).create!(:name => "#{association_name}_#{i}") }
before = @pirate.send(association_name).map { |c| c }
# Stub the save method of the first child to destroy and the second to raise an exception
class << before.first
def save(*args)
super
destroy
end
end
class << before.last
def save(*args)
super
raise 'Oh noes!'
end
end
assert_raise(RuntimeError) { assert !@pirate.save }
assert_equal before, @pirate.reload.send(association_name)
end
end
end
class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase
self.use_transactional_fixtures = false
def setup
@pirate = Pirate.create(:catchphrase => "Don' botharrr talkin' like one, savvy?")
@ship = @pirate.create_ship(:name => 'Nights Dirty Lightning')
end
def test_should_still_work_without_an_associated_model
@ship.destroy
@pirate.reload.catchphrase = "Arr"
@pirate.save
assert 'Arr', @pirate.reload.catchphrase
end
def test_should_automatically_save_the_associated_model
@pirate.ship.name = 'The Vile Insanity'
@pirate.save
assert_equal 'The Vile Insanity', @pirate.reload.ship.name
end
def test_should_automatically_validate_the_associated_model
@pirate.ship.name = ''
assert !@pirate.valid?
assert !@pirate.errors.on(:ship_name).blank?
end
def test_should_still_allow_to_bypass_validations_on_the_associated_model
@pirate.catchphrase = ''
@pirate.ship.name = ''
@pirate.save(false)
assert_equal ['', ''], [@pirate.reload.catchphrase, @pirate.ship.name]
end
def test_should_allow_to_bypass_validations_on_associated_models_at_any_depth
2.times { |i| @pirate.ship.parts.create!(:name => "part #{i}") }
@pirate.catchphrase = ''
@pirate.ship.name = ''
@pirate.ship.parts.each { |part| part.name = '' }
@pirate.save(false)
values = [@pirate.reload.catchphrase, @pirate.ship.name, *@pirate.ship.parts.map(&:name)]
assert_equal ['', '', '', ''], values
end
def test_should_still_raise_an_ActiveRecordRecord_Invalid_exception_if_we_want_that
@pirate.ship.name = ''
assert_raise(ActiveRecord::RecordInvalid) do
@pirate.save!
end
end
def test_should_rollback_any_changes_if_an_exception_occurred_while_saving
before = [@pirate.catchphrase, @pirate.ship.name]
@pirate.catchphrase = 'Arr'
@pirate.ship.name = 'The Vile Insanity'
# Stub the save method of the @pirate.ship instance to raise an exception
class << @pirate.ship
def save(*args)
super
raise 'Oh noes!'
end
end
assert_raise(RuntimeError) { assert !@pirate.save }
assert_equal before, [@pirate.reload.catchphrase, @pirate.ship.name]
end
def test_should_not_load_the_associated_model
assert_queries(1) { @pirate.catchphrase = 'Arr'; @pirate.save! }
end
end
class TestAutosaveAssociationOnABelongsToAssociation < ActiveRecord::TestCase
self.use_transactional_fixtures = false
def setup
@ship = Ship.create(:name => 'Nights Dirty Lightning')
@pirate = @ship.create_pirate(:catchphrase => "Don' botharrr talkin' like one, savvy?")
end
def test_should_still_work_without_an_associated_model
@pirate.destroy
@ship.reload.name = "The Vile Insanity"
@ship.save
assert 'The Vile Insanity', @ship.reload.name
end
def test_should_automatically_save_the_associated_model
@ship.pirate.catchphrase = 'Arr'
@ship.save
assert_equal 'Arr', @ship.reload.pirate.catchphrase
end
def test_should_automatically_validate_the_associated_model
@ship.pirate.catchphrase = ''
assert !@ship.valid?
assert !@ship.errors.on(:pirate_catchphrase).blank?
end
def test_should_still_allow_to_bypass_validations_on_the_associated_model
@ship.pirate.catchphrase = ''
@ship.name = ''
@ship.save(false)
assert_equal ['', ''], [@ship.reload.name, @ship.pirate.catchphrase]
end
def test_should_still_raise_an_ActiveRecordRecord_Invalid_exception_if_we_want_that
@ship.pirate.catchphrase = ''
assert_raise(ActiveRecord::RecordInvalid) do
@ship.save!
end
end
def test_should_rollback_any_changes_if_an_exception_occurred_while_saving
before = [@ship.pirate.catchphrase, @ship.name]
@ship.pirate.catchphrase = 'Arr'
@ship.name = 'The Vile Insanity'
# Stub the save method of the @ship.pirate instance to raise an exception
class << @ship.pirate
def save(*args)
super
raise 'Oh noes!'
end
end
assert_raise(RuntimeError) { assert !@ship.save }
# TODO: Why does using reload on @ship looses the associated pirate?
assert_equal before, [@ship.pirate.reload.catchphrase, @ship.reload.name]
end
def test_should_not_load_the_associated_model
assert_queries(1) { @ship.name = 'The Vile Insanity'; @ship.save! }
end
end
module AutosaveAssociationOnACollectionAssociationTests
def test_should_automatically_save_the_associated_models
new_names = ['Grace OMalley', 'Privateers Greed']
@pirate.send(@association_name).each_with_index { |child, i| child.name = new_names[i] }
@pirate.save
assert_equal new_names, @pirate.reload.send(@association_name).map(&:name)
end
def test_should_automatically_validate_the_associated_models
@pirate.send(@association_name).each { |child| child.name = '' }
assert !@pirate.valid?
assert_equal "can't be blank", @pirate.errors.on("#{@association_name}_name")
assert @pirate.errors.on(@association_name).blank?
end
def test_should_still_allow_to_bypass_validations_on_the_associated_models
@pirate.catchphrase = ''
@pirate.send(@association_name).each { |child| child.name = '' }
assert @pirate.save(false)
assert_equal ['', '', ''], [
@pirate.reload.catchphrase,
@pirate.send(@association_name).first.name,
@pirate.send(@association_name).last.name
]
end
def test_should_rollback_any_changes_if_an_exception_occurred_while_saving
before = [@pirate.catchphrase, *@pirate.send(@association_name).map(&:name)]
new_names = ['Grace OMalley', 'Privateers Greed']
@pirate.catchphrase = 'Arr'
@pirate.send(@association_name).each_with_index { |child, i| child.name = new_names[i] }
# Stub the save method of the first child instance to raise an exception
class << @pirate.send(@association_name).first
def save(*args)
super
raise 'Oh noes!'
end
end
assert_raise(RuntimeError) { assert !@pirate.save }
assert_equal before, [@pirate.reload.catchphrase, *@pirate.send(@association_name).map(&:name)]
end
def test_should_still_raise_an_ActiveRecordRecord_Invalid_exception_if_we_want_that
@pirate.send(@association_name).each { |child| child.name = '' }
assert_raise(ActiveRecord::RecordInvalid) do
@pirate.save!
end
end
def test_should_not_load_the_associated_models_if_they_were_not_loaded_yet
assert_queries(1) { @pirate.catchphrase = 'Arr'; @pirate.save! }
assert_queries(2) do
@pirate.catchphrase = 'Yarr'
new_names = ['Grace OMalley', 'Privateers Greed']
@pirate.send(@association_name).each_with_index { |child, i| child.name = new_names[i] }
@pirate.save!
end
end
end
class TestAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCase
self.use_transactional_fixtures = false
def setup
@association_name = :birds
@pirate = Pirate.create(:catchphrase => "Don' botharrr talkin' like one, savvy?")
@child_1 = @pirate.birds.create(:name => 'Posideons Killer')
@child_2 = @pirate.birds.create(:name => 'Killer bandita Dionne')
end
include AutosaveAssociationOnACollectionAssociationTests
end
class TestAutosaveAssociationOnAHasAndBelongsToManyAssociation < ActiveRecord::TestCase
self.use_transactional_fixtures = false
def setup
@association_name = :parrots
@habtm = true
@pirate = Pirate.create(:catchphrase => "Don' botharrr talkin' like one, savvy?")
@child_1 = @pirate.parrots.create(:name => 'Posideons Killer')
@child_2 = @pirate.parrots.create(:name => 'Killer bandita Dionne')
end
include AutosaveAssociationOnACollectionAssociationTests
end

View file

@ -16,6 +16,7 @@ require 'models/post'
require 'models/comment'
require 'models/minimalistic'
require 'models/warehouse_thing'
require 'models/parrot'
require 'rexml/document'
class Category < ActiveRecord::Base; end
@ -638,6 +639,13 @@ class BasicsTest < ActiveRecord::TestCase
category.reload
assert_not_nil category.categorizations_count
assert_equal 4, category.categorizations_count
category_2 = categories(:technology)
count_1, count_2 = (category.categorizations_count || 0), (category_2.categorizations_count || 0)
Category.update_counters([category.id, category_2.id], "categorizations_count" => 2)
category.reload; category_2.reload
assert_equal count_1 + 2, category.categorizations_count
assert_equal count_2 + 2, category_2.categorizations_count
end
def test_update_all
@ -1197,6 +1205,11 @@ class BasicsTest < ActiveRecord::TestCase
assert b_true.value?
end
def test_new_record_returns_boolean
assert_equal Topic.new.new_record?, true
assert_equal Topic.find(1).new_record?, false
end
def test_clone
topic = Topic.find(1)
cloned_topic = nil
@ -2071,6 +2084,15 @@ class BasicsTest < ActiveRecord::TestCase
ActiveRecord::Base.logger = original_logger
end
def test_create_with_custom_timestamps
custom_datetime = 1.hour.ago.beginning_of_day
%w(created_at created_on updated_at updated_on).each do |attribute|
parrot = LiveParrot.create(:name => "colombian", attribute => custom_datetime)
assert_equal custom_datetime, parrot[attribute]
end
end
private
def with_kcode(kcode)
if RUBY_VERSION < '1.9'

View file

@ -171,8 +171,9 @@ class CalculationsTest < ActiveRecord::TestCase
Account.expects(:columns).at_least_once.returns([column])
c = Account.count(:all, :group => :firm)
assert_equal Firm, c.first.first.class
assert_equal 1, c.first.last
first_key = c.keys.first
assert_equal Firm, first_key.class
assert_equal 1, c[first_key]
end
end

View file

@ -121,9 +121,19 @@ end
class CallbackCancellationDeveloper < ActiveRecord::Base
set_table_name 'developers'
def before_create
false
end
attr_reader :after_save_called, :after_create_called, :after_update_called, :after_destroy_called
attr_accessor :cancel_before_save, :cancel_before_create, :cancel_before_update, :cancel_before_destroy
def before_save; !@cancel_before_save; end
def before_create; !@cancel_before_create; end
def before_update; !@cancel_before_update; end
def before_destroy; !@cancel_before_destroy; end
def after_save; @after_save_called = true; end
def after_update; @after_update_called = true; end
def after_create; @after_create_called = true; end
def after_destroy; @after_destroy_called = true; end
end
class CallbacksTest < ActiveRecord::TestCase
@ -349,20 +359,48 @@ class CallbacksTest < ActiveRecord::TestCase
assert !david.valid?
assert !david.save
assert_raises(ActiveRecord::RecordInvalid) { david.save! }
someone = CallbackCancellationDeveloper.find(1)
someone.cancel_before_save = true
assert someone.valid?
assert !someone.save
assert_save_callbacks_not_called(someone)
end
def test_before_create_returning_false
someone = CallbackCancellationDeveloper.new
someone.cancel_before_create = true
assert someone.valid?
assert !someone.save
assert_save_callbacks_not_called(someone)
end
def test_before_update_returning_false
someone = CallbackCancellationDeveloper.find(1)
someone.cancel_before_update = true
assert someone.valid?
assert !someone.save
assert_save_callbacks_not_called(someone)
end
def test_before_destroy_returning_false
david = ImmutableDeveloper.find(1)
assert !david.destroy
assert_not_nil ImmutableDeveloper.find_by_id(1)
someone = CallbackCancellationDeveloper.find(1)
someone.cancel_before_destroy = true
assert !someone.destroy
assert !someone.after_destroy_called
end
def assert_save_callbacks_not_called(someone)
assert !someone.after_save_called
assert !someone.after_create_called
assert !someone.after_update_called
end
private :assert_save_callbacks_not_called
def test_zzz_callback_returning_false # must be run last since we modify CallbackDeveloper
david = CallbackDeveloper.find(1)
CallbackDeveloper.before_validation proc { |model| model.history << [:before_validation, :returning_false]; return false }

View file

@ -2,9 +2,24 @@ require "cases/helper"
class MysqlConnectionTest < ActiveRecord::TestCase
def setup
super
@connection = ActiveRecord::Base.connection
end
def test_mysql_reconnect_attribute_after_connection_with_reconnect_true
run_without_connection do |orig_connection|
ActiveRecord::Base.establish_connection(orig_connection.merge({:reconnect => true}))
assert ActiveRecord::Base.connection.raw_connection.reconnect
end
end
def test_mysql_reconnect_attribute_after_connection_with_reconnect_false
run_without_connection do |orig_connection|
ActiveRecord::Base.establish_connection(orig_connection.merge({:reconnect => false}))
assert !ActiveRecord::Base.connection.raw_connection.reconnect
end
end
def test_no_automatic_reconnection_after_timeout
assert @connection.active?
@connection.update('set @@wait_timeout=1')
@ -27,4 +42,15 @@ class MysqlConnectionTest < ActiveRecord::TestCase
@connection.verify!
assert @connection.active?
end
private
def run_without_connection
original_connection = ActiveRecord::Base.remove_connection
begin
yield original_connection
ensure
ActiveRecord::Base.establish_connection(original_connection)
end
end
end

View file

@ -46,6 +46,17 @@ class CopyTableTest < ActiveRecord::TestCase
test_copy_table('developers_projects', 'programmers_projects')
end
def test_copy_table_with_id_col_that_is_not_primary_key
test_copy_table('goofy_string_id', 'goofy_string_id2') do |from, to, options|
original_id = @connection.columns('goofy_string_id').detect{|col| col.name == 'id' }
copied_id = @connection.columns('goofy_string_id2').detect{|col| col.name == 'id' }
assert_equal original_id.type, copied_id.type
assert_equal original_id.sql_type, copied_id.sql_type
assert_equal original_id.limit, copied_id.limit
assert_equal original_id.primary, copied_id.primary
end
end
protected
def copy_table(from, to, options = {})
@connection.copy_table(from, to, {:temporary => true}.merge(options))

View file

@ -18,11 +18,43 @@ class DefaultTest < ActiveRecord::TestCase
end
end
if current_adapter?(:MysqlAdapter)
if current_adapter?(:PostgreSQLAdapter, :FirebirdAdapter, :OpenBaseAdapter, :OracleAdapter)
def test_default_integers
default = Default.new
assert_instance_of Fixnum, default.positive_integer
assert_equal 1, default.positive_integer
assert_instance_of Fixnum, default.negative_integer
assert_equal -1, default.negative_integer
assert_instance_of BigDecimal, default.decimal_number
assert_equal BigDecimal.new("2.78"), default.decimal_number
end
end
if current_adapter?(:PostgreSQLAdapter)
def test_multiline_default_text
# older postgres versions represent the default with escapes ("\\012" for a newline)
assert ( "--- []\n\n" == Default.columns_hash['multiline_default'].default ||
"--- []\\012\\012" == Default.columns_hash['multiline_default'].default)
end
end
end
#MySQL 5 and higher is quirky with not null text/blob columns.
#With MySQL Text/blob columns cannot have defaults. If the column is not null MySQL will report that the column has a null default
#but it behaves as though the column had a default of ''
if current_adapter?(:MysqlAdapter)
class DefaultsTestWithoutTransactionalFixtures < ActiveRecord::TestCase
# ActiveRecord::Base#create! (and #save and other related methods) will
# open a new transaction. When in transactional fixtures mode, this will
# cause ActiveRecord to create a new savepoint. However, since MySQL doesn't
# support DDL transactions, creating a table will result in any created
# savepoints to be automatically released. This in turn causes the savepoint
# release code in AbstractAdapter#transaction to fail.
#
# We don't want that to happen, so we disable transactional fixtures here.
self.use_transactional_fixtures = false
# MySQL 5 and higher is quirky with not null text/blob columns.
# With MySQL Text/blob columns cannot have defaults. If the column is not
# null MySQL will report that the column has a null default
# but it behaves as though the column had a default of ''
def test_mysql_text_not_null_defaults
klass = Class.new(ActiveRecord::Base)
klass.table_name = 'test_mysql_text_not_null_defaults'
@ -48,8 +80,7 @@ class DefaultTest < ActiveRecord::TestCase
ensure
klass.connection.drop_table(klass.table_name) rescue nil
end
# MySQL uses an implicit default 0 rather than NULL unless in strict mode.
# We use an implicit NULL so schema.rb is compatible with other databases.
def test_mysql_integer_not_null_defaults
@ -77,24 +108,4 @@ class DefaultTest < ActiveRecord::TestCase
klass.connection.drop_table(klass.table_name) rescue nil
end
end
if current_adapter?(:PostgreSQLAdapter, :FirebirdAdapter, :OpenBaseAdapter, :OracleAdapter)
def test_default_integers
default = Default.new
assert_instance_of Fixnum, default.positive_integer
assert_equal 1, default.positive_integer
assert_instance_of Fixnum, default.negative_integer
assert_equal -1, default.negative_integer
assert_instance_of BigDecimal, default.decimal_number
assert_equal BigDecimal.new("2.78"), default.decimal_number
end
end
if current_adapter?(:PostgreSQLAdapter)
def test_multiline_default_text
# older postgres versions represent the default with escapes ("\\012" for a newline)
assert ( "--- []\n\n" == Default.columns_hash['multiline_default'].default ||
"--- []\\012\\012" == Default.columns_hash['multiline_default'].default)
end
end
end

View file

@ -21,6 +21,10 @@ private
end
end
class NumericData < ActiveRecord::Base
self.table_name = 'numeric_data'
end
class DirtyTest < ActiveRecord::TestCase
def test_attribute_changes
# New record - no changes.
@ -58,7 +62,7 @@ class DirtyTest < ActiveRecord::TestCase
assert_equal parrot.name_change, parrot.title_change
end
def test_nullable_integer_not_marked_as_changed_if_new_value_is_blank
def test_nullable_number_not_marked_as_changed_if_new_value_is_blank
pirate = Pirate.new
["", nil].each do |value|
@ -68,6 +72,38 @@ class DirtyTest < ActiveRecord::TestCase
end
end
def test_nullable_decimal_not_marked_as_changed_if_new_value_is_blank
numeric_data = NumericData.new
["", nil].each do |value|
numeric_data.bank_balance = value
assert !numeric_data.bank_balance_changed?
assert_nil numeric_data.bank_balance_change
end
end
def test_nullable_float_not_marked_as_changed_if_new_value_is_blank
numeric_data = NumericData.new
["", nil].each do |value|
numeric_data.temperature = value
assert !numeric_data.temperature_changed?
assert_nil numeric_data.temperature_change
end
end
def test_nullable_integer_zero_to_string_zero_not_marked_as_changed
pirate = Pirate.new
pirate.parrot_id = 0
pirate.catchphrase = 'arrr'
assert pirate.save!
assert !pirate.changed?
pirate.parrot_id = '0'
assert !pirate.changed?
end
def test_zero_to_blank_marked_as_changed
pirate = Pirate.new
pirate.catchphrase = "Yarrrr, me hearties"
@ -130,7 +166,7 @@ class DirtyTest < ActiveRecord::TestCase
def test_association_assignment_changes_foreign_key
pirate = Pirate.create!(:catchphrase => 'jarl')
pirate.parrot = Parrot.create!
pirate.parrot = Parrot.create!(:name => 'Lorre')
assert pirate.changed?
assert_equal %w(parrot_id), pirate.changed
end

View file

@ -175,6 +175,13 @@ class FinderTest < ActiveRecord::TestCase
assert_equal 4, developers.map(&:salary).uniq.size
end
def test_find_with_group_and_having
developers = Developer.find(:all, :group => "salary", :having => "sum(salary) > 10000", :select => "salary")
assert_equal 3, developers.size
assert_equal 3, developers.map(&:salary).uniq.size
assert developers.all? { |developer| developer.salary > 10000 }
end
def test_find_with_entire_select_statement
topics = Topic.find_by_sql "SELECT * FROM topics WHERE author_name = 'Mary'"

View file

@ -641,15 +641,15 @@ end
class FixtureLoadingTest < ActiveRecord::TestCase
uses_mocha 'reloading_fixtures_through_accessor_methods' do
def test_logs_message_for_failed_dependency_load
Test::Unit::TestCase.expects(:require_dependency).with(:does_not_exist).raises(LoadError)
ActiveRecord::TestCase.expects(:require_dependency).with(:does_not_exist).raises(LoadError)
ActiveRecord::Base.logger.expects(:warn)
Test::Unit::TestCase.try_to_load_dependency(:does_not_exist)
ActiveRecord::TestCase.try_to_load_dependency(:does_not_exist)
end
def test_does_not_logs_message_for_successful_dependency_load
Test::Unit::TestCase.expects(:require_dependency).with(:works_out_fine)
ActiveRecord::TestCase.expects(:require_dependency).with(:works_out_fine)
ActiveRecord::Base.logger.expects(:warn).never
Test::Unit::TestCase.try_to_load_dependency(:works_out_fine)
ActiveRecord::TestCase.try_to_load_dependency(:works_out_fine)
end
end
end

View file

@ -5,10 +5,12 @@ require 'config'
require 'test/unit'
require 'active_record'
require 'active_record/fixtures'
require 'active_record/test_case'
require 'active_record/fixtures'
require 'connection'
require 'cases/repair_helper'
# Show backtraces for deprecated behavior for quicker cleanup.
ActiveSupport::Deprecation.debug = true
@ -24,6 +26,7 @@ end
def uses_mocha(description)
require 'rubygems'
gem 'mocha', '>= 0.9.3'
require 'mocha'
yield
rescue LoadError
@ -31,7 +34,7 @@ rescue LoadError
end
ActiveRecord::Base.connection.class.class_eval do
IGNORED_SQL = [/^PRAGMA/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/]
IGNORED_SQL = [/^PRAGMA/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/, /^SAVEPOINT/, /^ROLLBACK TO SAVEPOINT/, /^RELEASE SAVEPOINT/, /SHOW FIELDS/]
def execute_with_query_record(sql, name = nil, &block)
$queries_executed ||= []
@ -48,15 +51,24 @@ class << ActiveRecord::Base
end
unless ENV['FIXTURE_DEBUG']
module Test #:nodoc:
module Unit #:nodoc:
class << TestCase #:nodoc:
def try_to_load_dependency_with_silence(*args)
ActiveRecord::Base.logger.silence { try_to_load_dependency_without_silence(*args)}
end
alias_method_chain :try_to_load_dependency, :silence
end
module ActiveRecord::TestFixtures::ClassMethods
def try_to_load_dependency_with_silence(*args)
ActiveRecord::Base.logger.silence { try_to_load_dependency_without_silence(*args)}
end
alias_method_chain :try_to_load_dependency, :silence
end
end
end
class ActiveSupport::TestCase
include ActiveRecord::TestFixtures
include ActiveRecord::Testing::RepairHelper
self.fixture_path = FIXTURES_ROOT
self.use_instantiated_fixtures = false
self.use_transactional_fixtures = true
def create_fixtures(*table_names, &block)
Fixtures.create_fixtures(ActiveSupport::TestCase.fixture_path, table_names, {}, &block)
end
end

View file

@ -200,6 +200,6 @@ class DatabaseConnectedJsonEncodingTest < ActiveRecord::TestCase
2 => @mary
}
assert_equal %({1: {"name": "David"}}), authors_hash.to_json(:only => [1, :name])
assert_equal %({"1": {"name": "David"}}), authors_hash.to_json(:only => [1, :name])
end
end

View file

@ -27,6 +27,24 @@ class MethodScopingTest < ActiveRecord::TestCase
end
end
def test_scoped_find_last
highest_salary = Developer.find(:first, :order => "salary DESC")
Developer.with_scope(:find => { :order => "salary" }) do
assert_equal highest_salary, Developer.last
end
end
def test_scoped_find_last_preserves_scope
lowest_salary = Developer.find(:first, :order => "salary ASC")
highest_salary = Developer.find(:first, :order => "salary DESC")
Developer.with_scope(:find => { :order => "salary" }) do
assert_equal highest_salary, Developer.last
assert_equal lowest_salary, Developer.first
end
end
def test_scoped_find_combines_conditions
Developer.with_scope(:find => { :conditions => "salary = 9000" }) do
assert_equal developers(:poor_jamis), Developer.find(:first, :conditions => "name = 'Jamis'")
@ -168,6 +186,16 @@ class MethodScopingTest < ActiveRecord::TestCase
assert_equal authors(:david).attributes, scoped_authors.first.attributes
end
def test_scoped_find_strips_spaces_from_string_joins_and_eliminates_duplicate_string_joins
scoped_authors = Author.with_scope(:find => { :joins => ' INNER JOIN posts ON posts.author_id = authors.id '}) do
Author.find(:all, :select => 'DISTINCT authors.*', :joins => ['INNER JOIN posts ON posts.author_id = authors.id'], :conditions => 'posts.id = 1')
end
assert scoped_authors.include?(authors(:david))
assert !scoped_authors.include?(authors(:mary))
assert_equal 1, scoped_authors.size
assert_equal authors(:david).attributes, scoped_authors.first.attributes
end
def test_scoped_count_include
# with the include, will retrieve only developers for the given project
Developer.with_scope(:find => { :include => :projects }) do
@ -522,6 +550,73 @@ class HasAndBelongsToManyScopingTest< ActiveRecord::TestCase
end
class DefaultScopingTest < ActiveRecord::TestCase
fixtures :developers
def test_default_scope
expected = Developer.find(:all, :order => 'salary DESC').collect { |dev| dev.salary }
received = DeveloperOrderedBySalary.find(:all).collect { |dev| dev.salary }
assert_equal expected, received
end
def test_default_scoping_with_threads
scope = [{ :create => {}, :find => { :order => 'salary DESC' } }]
2.times do
Thread.new { assert_equal scope, DeveloperOrderedBySalary.send(:scoped_methods) }.join
end
end
def test_default_scoping_with_inheritance
scope = [{ :create => {}, :find => { :order => 'salary DESC' } }]
# Inherit a class having a default scope and define a new default scope
klass = Class.new(DeveloperOrderedBySalary)
klass.send :default_scope, {}
# Scopes added on children should append to parent scope
expected_klass_scope = [{ :create => {}, :find => { :order => 'salary DESC' }}, { :create => {}, :find => {} }]
assert_equal expected_klass_scope, klass.send(:scoped_methods)
# Parent should still have the original scope
assert_equal scope, DeveloperOrderedBySalary.send(:scoped_methods)
end
def test_method_scope
expected = Developer.find(:all, :order => 'name DESC').collect { |dev| dev.salary }
received = DeveloperOrderedBySalary.all_ordered_by_name.collect { |dev| dev.salary }
assert_equal expected, received
end
def test_nested_scope
expected = Developer.find(:all, :order => 'name DESC').collect { |dev| dev.salary }
received = DeveloperOrderedBySalary.with_scope(:find => { :order => 'name DESC'}) do
DeveloperOrderedBySalary.find(:all).collect { |dev| dev.salary }
end
assert_equal expected, received
end
def test_named_scope
expected = Developer.find(:all, :order => 'name DESC').collect { |dev| dev.salary }
received = DeveloperOrderedBySalary.by_name.find(:all).collect { |dev| dev.salary }
assert_equal expected, received
end
def test_nested_exclusive_scope
expected = Developer.find(:all, :limit => 100).collect { |dev| dev.salary }
received = DeveloperOrderedBySalary.with_exclusive_scope(:find => { :limit => 100 }) do
DeveloperOrderedBySalary.find(:all).collect { |dev| dev.salary }
end
assert_equal expected, received
end
def test_overwriting_default_scope
expected = Developer.find(:all, :order => 'salary').collect { |dev| dev.salary }
received = DeveloperOrderedBySalary.find(:all, :order => 'salary').collect { |dev| dev.salary }
assert_equal expected, received
end
end
=begin
# We disabled the scoping for has_one and belongs_to as we can't think of a proper use case

View file

@ -254,7 +254,7 @@ class NamedScopeTest < ActiveRecord::TestCase
end
def test_should_use_where_in_query_for_named_scope
assert_equal Developer.find_all_by_name('Jamis'), Developer.find_all_by_id(Developer.jamises)
assert_equal Developer.find_all_by_name('Jamis').to_set, Developer.find_all_by_id(Developer.jamises).to_set
end
def test_size_should_use_count_when_results_are_not_loaded
@ -277,4 +277,44 @@ class NamedScopeTest < ActiveRecord::TestCase
post = Post.find(1)
assert_equal post.comments.size, Post.scoped(:joins => join).scoped(:joins => join, :conditions => "posts.id = #{post.id}").size
end
def test_chanining_should_use_latest_conditions_when_creating
post1 = Topic.rejected.approved.new
assert post1.approved?
post2 = Topic.approved.rejected.new
assert ! post2.approved?
end
def test_chanining_should_use_latest_conditions_when_searching
# Normal hash conditions
assert_equal Topic.all(:conditions => {:approved => true}), Topic.rejected.approved.all
assert_equal Topic.all(:conditions => {:approved => false}), Topic.approved.rejected.all
# Nested hash conditions with same keys
assert_equal [posts(:sti_comments)], Post.with_special_comments.with_very_special_comments.all
# Nested hash conditions with different keys
assert_equal [posts(:sti_comments)], Post.with_special_comments.with_post(4).all.uniq
end
end
class DynamicScopeMatchTest < ActiveRecord::TestCase
def test_scoped_by_no_match
assert_nil ActiveRecord::DynamicScopeMatch.match("not_scoped_at_all")
end
def test_scoped_by
match = ActiveRecord::DynamicScopeMatch.match("scoped_by_age_and_sex_and_location")
assert_not_nil match
assert match.scope?
assert_equal %w(age sex location), match.attribute_names
end
end
class DynamicScopeTest < ActiveRecord::TestCase
def test_dynamic_scope
assert_equal Post.scoped_by_author_id(1).find(1), Post.find(1)
assert_equal Post.scoped_by_author_id_and_title(1, "Welcome to the weblog").first, Post.find(:first, :conditions => { :author_id => 1, :title => "Welcome to the weblog"})
end
end

View file

@ -0,0 +1,359 @@
require "cases/helper"
require "models/pirate"
require "models/ship"
require "models/bird"
require "models/parrot"
require "models/treasure"
module AssertRaiseWithMessage
def assert_raise_with_message(expected_exception, expected_message)
begin
error_raised = false
yield
rescue expected_exception => error
error_raised = true
actual_message = error.message
end
assert error_raised
assert_equal expected_message, actual_message
end
end
class TestNestedAttributesInGeneral < ActiveRecord::TestCase
include AssertRaiseWithMessage
def teardown
Pirate.accepts_nested_attributes_for :ship, :allow_destroy => true
end
def test_base_should_have_an_empty_reject_new_nested_attributes_procs
assert_equal Hash.new, ActiveRecord::Base.reject_new_nested_attributes_procs
end
def test_should_add_a_proc_to_reject_new_nested_attributes_procs
[:parrots, :birds].each do |name|
assert_instance_of Proc, Pirate.reject_new_nested_attributes_procs[name]
end
end
def test_should_raise_an_ArgumentError_for_non_existing_associations
assert_raise_with_message ArgumentError, "No association found for name `honesty'. Has it been defined yet?" do
Pirate.accepts_nested_attributes_for :honesty
end
end
def test_should_disable_allow_destroy_by_default
Pirate.accepts_nested_attributes_for :ship
pirate = Pirate.create!(:catchphrase => "Don' botharrr talkin' like one, savvy?")
ship = pirate.create_ship(:name => 'Nights Dirty Lightning')
assert_no_difference('Ship.count') do
pirate.update_attributes(:ship_attributes => { '_delete' => true })
end
end
def test_a_model_should_respond_to_underscore_delete_and_return_if_it_is_marked_for_destruction
ship = Ship.create!(:name => 'Nights Dirty Lightning')
assert !ship._delete
ship.mark_for_destruction
assert ship._delete
end
end
class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase
def setup
@pirate = Pirate.create!(:catchphrase => "Don' botharrr talkin' like one, savvy?")
@ship = @pirate.create_ship(:name => 'Nights Dirty Lightning')
end
def test_should_define_an_attribute_writer_method_for_the_association
assert_respond_to @pirate, :ship_attributes=
end
def test_should_automatically_instantiate_an_associated_model_if_there_is_none
@ship.destroy
@pirate.reload.ship_attributes = { :name => 'Davy Jones Gold Dagger' }
assert @pirate.ship.new_record?
assert_equal 'Davy Jones Gold Dagger', @pirate.ship.name
end
def test_should_take_a_hash_and_assign_the_attributes_to_the_existing_associated_model
@pirate.ship_attributes = { :name => 'Davy Jones Gold Dagger' }
assert !@pirate.ship.new_record?
assert_equal 'Davy Jones Gold Dagger', @pirate.ship.name
end
def test_should_also_work_with_a_HashWithIndifferentAccess
@pirate.ship_attributes = HashWithIndifferentAccess.new(:name => 'Davy Jones Gold Dagger')
assert !@pirate.ship.new_record?
assert_equal 'Davy Jones Gold Dagger', @pirate.ship.name
end
def test_should_work_with_update_attributes_as_well
@pirate.update_attributes({ :catchphrase => 'Arr', :ship_attributes => { :name => 'Mister Pablo' } })
@pirate.reload
assert_equal 'Arr', @pirate.catchphrase
assert_equal 'Mister Pablo', @pirate.ship.name
end
def test_should_be_possible_to_destroy_the_associated_model
@pirate.ship.destroy
['1', 1, 'true', true].each do |true_variable|
@pirate.reload.create_ship(:name => 'Mister Pablo')
assert_difference('Ship.count', -1) do
@pirate.update_attributes(:ship_attributes => { '_delete' => true_variable })
end
end
end
def test_should_not_destroy_the_associated_model_with_a_non_truthy_argument
[nil, '0', 0, 'false', false].each do |false_variable|
assert_no_difference('Ship.count') do
@pirate.update_attributes(:ship_attributes => { '_delete' => false_variable })
end
end
end
def test_should_not_destroy_the_associated_model_until_the_parent_is_saved
assert_no_difference('Ship.count') do
@pirate.attributes = { :ship_attributes => { '_delete' => true } }
end
assert_difference('Ship.count', -1) { @pirate.save }
end
def test_should_automatically_enable_autosave_on_the_association
assert Pirate.reflect_on_association(:ship).options[:autosave]
end
end
class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase
def setup
@ship = Ship.create!(:name => 'Nights Dirty Lightning')
@pirate = @ship.create_pirate(:catchphrase => "Don' botharrr talkin' like one, savvy?")
end
def test_should_define_an_attribute_writer_method_for_the_association
assert_respond_to @ship, :pirate_attributes=
end
def test_should_automatically_instantiate_an_associated_model_if_there_is_none
@pirate.destroy
@ship.reload.pirate_attributes = { :catchphrase => 'Arr' }
assert @ship.pirate.new_record?
assert_equal 'Arr', @ship.pirate.catchphrase
end
def test_should_take_a_hash_and_assign_the_attributes_to_the_existing_associated_model
@ship.pirate_attributes = { :catchphrase => 'Arr' }
assert !@ship.pirate.new_record?
assert_equal 'Arr', @ship.pirate.catchphrase
end
def test_should_also_work_with_a_HashWithIndifferentAccess
@ship.pirate_attributes = HashWithIndifferentAccess.new(:catchphrase => 'Arr')
assert !@ship.pirate.new_record?
assert_equal 'Arr', @ship.pirate.catchphrase
end
def test_should_work_with_update_attributes_as_well
@ship.update_attributes({ :name => 'Mister Pablo', :pirate_attributes => { :catchphrase => 'Arr' } })
@ship.reload
assert_equal 'Mister Pablo', @ship.name
assert_equal 'Arr', @ship.pirate.catchphrase
end
def test_should_be_possible_to_destroy_the_associated_model
@ship.pirate.destroy
['1', 1, 'true', true].each do |true_variable|
@ship.reload.create_pirate(:catchphrase => 'Arr')
assert_difference('Pirate.count', -1) do
@ship.update_attributes(:pirate_attributes => { '_delete' => true_variable })
end
end
end
def test_should_not_destroy_the_associated_model_with_a_non_truthy_argument
[nil, '', '0', 0, 'false', false].each do |false_variable|
assert_no_difference('Pirate.count') do
@ship.update_attributes(:pirate_attributes => { '_delete' => false_variable })
end
end
end
def test_should_not_destroy_the_associated_model_until_the_parent_is_saved
assert_no_difference('Pirate.count') do
@ship.attributes = { :pirate_attributes => { '_delete' => true } }
end
assert_difference('Pirate.count', -1) { @ship.save }
end
def test_should_automatically_enable_autosave_on_the_association
assert Ship.reflect_on_association(:pirate).options[:autosave]
end
end
module NestedAttributesOnACollectionAssociationTests
include AssertRaiseWithMessage
def test_should_define_an_attribute_writer_method_for_the_association
assert_respond_to @pirate, association_setter
end
def test_should_take_a_hash_with_string_keys_and_assign_the_attributes_to_the_associated_models
@alternate_params[association_getter].stringify_keys!
@pirate.update_attributes @alternate_params
assert_equal ['Grace OMalley', 'Privateers Greed'], [@child_1.reload.name, @child_2.reload.name]
end
def test_should_also_work_with_a_HashWithIndifferentAccess
@pirate.send(association_setter, HashWithIndifferentAccess.new(@child_1.id => HashWithIndifferentAccess.new(:name => 'Grace OMalley')))
@pirate.save
assert_equal 'Grace OMalley', @child_1.reload.name
end
def test_should_take_a_hash_with_integer_keys_and_assign_the_attributes_to_the_associated_models
@pirate.attributes = @alternate_params
assert_equal 'Grace OMalley', @pirate.send(@association_name).first.name
assert_equal 'Privateers Greed', @pirate.send(@association_name).last.name
end
def test_should_automatically_build_new_associated_models_for_each_entry_in_a_hash_where_the_id_starts_with_the_string_new_
@pirate.send(@association_name).destroy_all
@pirate.reload.attributes = { association_getter => { 'new_1' => { :name => 'Grace OMalley' }, 'new_2' => { :name => 'Privateers Greed' }}}
assert @pirate.send(@association_name).first.new_record?
assert_equal 'Grace OMalley', @pirate.send(@association_name).first.name
assert @pirate.send(@association_name).last.new_record?
assert_equal 'Privateers Greed', @pirate.send(@association_name).last.name
end
def test_should_sort_the_hash_by_the_keys_before_building_new_associated_models
attributes = ActiveSupport::OrderedHash.new
attributes['new_123726353'] = { :name => 'Grace OMalley' }
attributes['new_2'] = { :name => 'Privateers Greed' } # 2 is lower then 123726353
@pirate.send(association_setter, attributes)
assert_equal ['Posideons Killer', 'Killer bandita Dionne', 'Privateers Greed', 'Grace OMalley'].to_set, @pirate.send(@association_name).map(&:name).to_set
end
def test_should_raise_an_argument_error_if_something_else_than_a_hash_is_passed
assert_nothing_raised(ArgumentError) { @pirate.send(association_setter, {}) }
assert_nothing_raised(ArgumentError) { @pirate.send(association_setter, ActiveSupport::OrderedHash.new) }
assert_raise_with_message ArgumentError, 'Hash expected, got String ("foo")' do
@pirate.send(association_setter, "foo")
end
assert_raise_with_message ArgumentError, 'Hash expected, got Array ([:foo, :bar])' do
@pirate.send(association_setter, [:foo, :bar])
end
end
def test_should_work_with_update_attributes_as_well
@pirate.update_attributes({ :catchphrase => 'Arr', association_getter => { @child_1.id => { :name => 'Grace OMalley' }}})
assert_equal 'Grace OMalley', @child_1.reload.name
end
def test_should_automatically_reject_any_new_record_if_a_reject_if_proc_exists_and_returns_false
@alternate_params[association_getter]["new_12345"] = {}
assert_no_difference("@pirate.send(@association_name).length") do
@pirate.attributes = @alternate_params
end
end
def test_should_update_existing_records_and_add_new_ones_that_have_an_id_that_start_with_the_string_new_
@alternate_params[association_getter]['new_12345'] = { :name => 'Buccaneers Servant' }
assert_difference('@pirate.send(@association_name).count', +1) do
@pirate.update_attributes @alternate_params
end
assert_equal ['Grace OMalley', 'Privateers Greed', 'Buccaneers Servant'].to_set, @pirate.reload.send(@association_name).map(&:name).to_set
end
def test_should_be_possible_to_destroy_a_record
['1', 1, 'true', true].each do |true_variable|
record = @pirate.reload.send(@association_name).create!(:name => 'Grace OMalley')
@pirate.send(association_setter,
@alternate_params[association_getter].merge(record.id => { '_delete' => true_variable })
)
assert_difference('@pirate.send(@association_name).count', -1) do
@pirate.save
end
end
end
def test_should_not_destroy_the_associated_model_with_a_non_truthy_argument
[nil, '', '0', 0, 'false', false].each do |false_variable|
@alternate_params[association_getter][@child_1.id]['_delete'] = false_variable
assert_no_difference('@pirate.send(@association_name).count') do
@pirate.update_attributes(@alternate_params)
end
end
end
def test_should_not_destroy_the_associated_model_until_the_parent_is_saved
assert_no_difference('@pirate.send(@association_name).count') do
@pirate.send(association_setter, @alternate_params[association_getter].merge(@child_1.id => { '_delete' => true }))
end
assert_difference('@pirate.send(@association_name).count', -1) { @pirate.save }
end
def test_should_automatically_enable_autosave_on_the_association
assert Pirate.reflect_on_association(@association_name).options[:autosave]
end
private
def association_setter
@association_setter ||= "#{@association_name}_attributes=".to_sym
end
def association_getter
@association_getter ||= "#{@association_name}_attributes".to_sym
end
end
class TestNestedAttributesOnAHasManyAssociation < ActiveRecord::TestCase
def setup
@association_type = :has_many
@association_name = :birds
@pirate = Pirate.create!(:catchphrase => "Don' botharrr talkin' like one, savvy?")
@child_1 = @pirate.birds.create!(:name => 'Posideons Killer')
@child_2 = @pirate.birds.create!(:name => 'Killer bandita Dionne')
@alternate_params = {
:birds_attributes => {
@child_1.id => { :name => 'Grace OMalley' },
@child_2.id => { :name => 'Privateers Greed' }
}
}
end
include NestedAttributesOnACollectionAssociationTests
end
class TestNestedAttributesOnAHasAndBelongsToManyAssociation < ActiveRecord::TestCase
def setup
@association_type = :has_and_belongs_to_many
@association_name = :parrots
@pirate = Pirate.create!(:catchphrase => "Don' botharrr talkin' like one, savvy?")
@child_1 = @pirate.parrots.create!(:name => 'Posideons Killer')
@child_2 = @pirate.parrots.create!(:name => 'Killer bandita Dionne')
@alternate_params = {
:parrots_attributes => {
@child_1.id => { :name => 'Grace OMalley' },
@child_2.id => { :name => 'Privateers Greed' }
}
}
end
include NestedAttributesOnACollectionAssociationTests
end

View file

@ -91,6 +91,15 @@ class ReflectionTest < ActiveRecord::TestCase
assert_equal Money, Customer.reflect_on_aggregation(:balance).klass
end
def test_reflect_on_all_autosave_associations
expected = Pirate.reflect_on_all_associations.select { |r| r.options[:autosave] }
received = Pirate.reflect_on_all_autosave_associations
assert !received.empty?
assert_not_equal Pirate.reflect_on_all_associations.length, received.length
assert_equal expected, received
end
def test_has_many_reflection
reflection_for_clients = ActiveRecord::Reflection::AssociationReflection.new(:has_many, :clients, { :order => "id", :dependent => :destroy }, Firm)

View file

@ -3,6 +3,8 @@ require 'models/owner'
require 'models/pet'
class ReloadModelsTest < ActiveRecord::TestCase
fixtures :pets
def test_has_one_with_reload
pet = Pet.find_by_name('parrot')
pet.owner = Owner.find_by_name('ashley')
@ -17,4 +19,4 @@ class ReloadModelsTest < ActiveRecord::TestCase
pet.owner = Owner.find_by_name('ashley')
assert_equal pet.owner, Owner.find_by_name('ashley')
end
end
end

View file

@ -0,0 +1,50 @@
module ActiveRecord
module Testing
module RepairHelper
def self.included(base)
base.class_eval do
extend ClassMethods
end
end
module Toolbox
def self.record_validations(*model_classes)
model_classes.inject({}) do |repair, klass|
repair[klass] ||= {}
[:validate, :validate_on_create, :validate_on_update].each do |callback|
the_callback = klass.instance_variable_get("@#{callback.to_s}_callbacks")
repair[klass][callback] = (the_callback.nil? ? nil : the_callback.dup)
end
repair
end
end
def self.reset_validations(recorded)
recorded.each do |klass, repairs|
[:validate, :validate_on_create, :validate_on_update].each do |callback|
klass.instance_variable_set("@#{callback.to_s}_callbacks", repairs[callback])
end
end
end
end
module ClassMethods
def repair_validations(*model_classes)
setup do
@validation_repairs = ActiveRecord::Testing::RepairHelper::Toolbox.record_validations(*model_classes)
end
teardown do
ActiveRecord::Testing::RepairHelper::Toolbox.reset_validations(@validation_repairs)
end
end
end
def repair_validations(*model_classes, &block)
validation_repairs = ActiveRecord::Testing::RepairHelper::Toolbox.record_validations(*model_classes)
return block.call
ensure
ActiveRecord::Testing::RepairHelper::Toolbox.reset_validations(validation_repairs)
end
end
end
end

View file

@ -1,5 +1,4 @@
require "cases/helper"
require 'active_record/schema_dumper'
require 'stringio'

View file

@ -213,11 +213,104 @@ class TransactionTest < ActiveRecord::TestCase
assert Topic.find(2).approved?, "Second should still be approved"
end
def test_invalid_keys_for_transaction
assert_raises ArgumentError do
Topic.transaction :nested => true do
end
end
end
def test_force_savepoint_in_nested_transaction
Topic.transaction do
@first.approved = true
@second.approved = false
@first.save!
@second.save!
begin
Topic.transaction :requires_new => true do
@first.happy = false
@first.save!
raise
end
rescue
end
end
assert @first.reload.approved?
assert !@second.reload.approved?
end if Topic.connection.supports_savepoints?
def test_no_savepoint_in_nested_transaction_without_force
Topic.transaction do
@first.approved = true
@second.approved = false
@first.save!
@second.save!
begin
Topic.transaction do
@first.approved = false
@first.save!
raise
end
rescue
end
end
assert !@first.reload.approved?
assert !@second.reload.approved?
end if Topic.connection.supports_savepoints?
def test_many_savepoints
Topic.transaction do
@first.content = "One"
@first.save!
begin
Topic.transaction :requires_new => true do
@first.content = "Two"
@first.save!
begin
Topic.transaction :requires_new => true do
@first.content = "Three"
@first.save!
begin
Topic.transaction :requires_new => true do
@first.content = "Four"
@first.save!
raise
end
rescue
end
@three = @first.reload.content
raise
end
rescue
end
@two = @first.reload.content
raise
end
rescue
end
@one = @first.reload.content
end
assert_equal "One", @one
assert_equal "Two", @two
assert_equal "Three", @three
end if Topic.connection.supports_savepoints?
uses_mocha 'mocking connection.commit_db_transaction' do
def test_rollback_when_commit_raises
Topic.connection.expects(:begin_db_transaction)
Topic.connection.expects(:transaction_active?).returns(true) if current_adapter?(:PostgreSQLAdapter)
Topic.connection.expects(:commit_db_transaction).raises('OH NOES')
Topic.connection.expects(:outside_transaction?).returns(false)
Topic.connection.expects(:rollback_db_transaction)
assert_raise RuntimeError do
@ -227,6 +320,38 @@ class TransactionTest < ActiveRecord::TestCase
end
end
end
if current_adapter?(:PostgreSQLAdapter) && defined?(PGconn::PQTRANS_IDLE)
def test_outside_transaction_works
assert Topic.connection.outside_transaction?
Topic.connection.begin_db_transaction
assert !Topic.connection.outside_transaction?
Topic.connection.rollback_db_transaction
assert Topic.connection.outside_transaction?
end
uses_mocha 'mocking connection.rollback_db_transaction' do
def test_rollback_wont_be_executed_if_no_transaction_active
assert_raise RuntimeError do
Topic.transaction do
Topic.connection.rollback_db_transaction
Topic.connection.expects(:rollback_db_transaction).never
raise "Rails doesn't scale!"
end
end
end
end
def test_open_transactions_count_is_reset_to_zero_if_no_transaction_active
Topic.transaction do
Topic.transaction do
Topic.connection.rollback_db_transaction
end
assert_equal 0, Topic.connection.open_transactions
end
assert_equal 0, Topic.connection.open_transactions
end
end
def test_sqlite_add_column_in_transaction_raises_statement_invalid
return true unless current_adapter?(:SQLite3Adapter, :SQLiteAdapter)
@ -282,6 +407,45 @@ class TransactionTest < ActiveRecord::TestCase
end
end
class TransactionsWithTransactionalFixturesTest < ActiveRecord::TestCase
self.use_transactional_fixtures = true
fixtures :topics
def test_automatic_savepoint_in_outer_transaction
@first = Topic.find(1)
begin
Topic.transaction do
@first.approved = true
@first.save!
raise
end
rescue
assert !@first.reload.approved?
end
end
def test_no_automatic_savepoint_for_inner_transaction
@first = Topic.find(1)
Topic.transaction do
@first.approved = true
@first.save!
begin
Topic.transaction do
@first.approved = false
@first.save!
raise
end
rescue
end
end
assert !@first.reload.approved?
end
end if Topic.connection.supports_savepoints?
if current_adapter?(:PostgreSQLAdapter)
class ConcurrentTransactionTest < TransactionTest
use_concurrent_connections

View file

@ -2,7 +2,7 @@ require "cases/helper"
require 'models/topic'
require 'models/reply'
class ActiveRecordValidationsI18nTests < Test::Unit::TestCase
class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase
def setup
reset_callbacks Topic
@topic = Topic.new
@ -506,7 +506,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase
# validates_length_of :is w/o mocha
def test_validates_length_of_within_finds_custom_model_key_translation
def test_validates_length_of_is_finds_custom_model_key_translation
I18n.backend.store_translations 'en', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:wrong_length => 'custom message'}}}}}}
I18n.backend.store_translations 'en', :activerecord => {:errors => {:messages => {:wrong_length => 'global message'}}}
@ -515,7 +515,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase
assert_equal 'custom message', @topic.errors.on(:title)
end
def test_validates_length_of_within_finds_global_default_translation
def test_validates_length_of_is_finds_global_default_translation
I18n.backend.store_translations 'en', :activerecord => {:errors => {:messages => {:wrong_length => 'global message'}}}
Topic.validates_length_of :title, :is => 5
@ -525,7 +525,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase
# validates_uniqueness_of w/o mocha
def test_validates_length_of_within_finds_custom_model_key_translation
def test_validates_length_of_is_finds_custom_model_key_translation
I18n.backend.store_translations 'en', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:wrong_length => 'custom message'}}}}}}
I18n.backend.store_translations 'en', :activerecord => {:errors => {:messages => {:wrong_length => 'global message'}}}
@ -534,7 +534,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase
assert_equal 'custom message', @topic.errors.on(:title)
end
def test_validates_length_of_within_finds_global_default_translation
def test_validates_length_of_is_finds_global_default_translation
I18n.backend.store_translations 'en', :activerecord => {:errors => {:messages => {:wrong_length => 'global message'}}}
Topic.validates_length_of :title, :is => 5

View file

@ -6,6 +6,8 @@ require 'models/person'
require 'models/developer'
require 'models/warehouse_thing'
require 'models/guid'
require 'models/owner'
require 'models/pet'
# The following methods in Topic are used in test_conditional_validation_*
class Topic
@ -31,10 +33,6 @@ class UniqueReply < Reply
validates_uniqueness_of :content, :scope => 'parent_id'
end
class PlagiarizedReply < Reply
validates_acceptance_of :author_name
end
class SillyUniqueReply < UniqueReply
end
@ -58,11 +56,9 @@ end
class ValidationsTest < ActiveRecord::TestCase
fixtures :topics, :developers, 'warehouse-things'
def setup
Topic.instance_variable_set("@validate_callbacks", ActiveSupport::Callbacks::CallbackChain.new)
Topic.instance_variable_set("@validate_on_create_callbacks", ActiveSupport::Callbacks::CallbackChain.new)
Topic.instance_variable_set("@validate_on_update_callbacks", ActiveSupport::Callbacks::CallbackChain.new)
end
# Most of the tests mess with the validations of Topic, so lets repair it all the time.
# Other classes we mess with will be dealt with in the specific tests
repair_validations(Topic)
def test_single_field_validation
r = Reply.new
@ -134,7 +130,7 @@ class ValidationsTest < ActiveRecord::TestCase
Reply.create!([ { "title" => "OK" }, { "title" => "Wrong Create" }])
end
end
def test_exception_on_create_bang_with_block
assert_raises(ActiveRecord::RecordInvalid) do
Reply.create!({ "title" => "OK" }) do |r|
@ -142,7 +138,7 @@ class ValidationsTest < ActiveRecord::TestCase
end
end
end
def test_exception_on_create_bang_many_with_block
assert_raises(ActiveRecord::RecordInvalid) do
Reply.create!([{ "title" => "OK" }, { "title" => "Wrong Create" }]) do |r|
@ -229,21 +225,16 @@ class ValidationsTest < ActiveRecord::TestCase
end
def test_validates_each
perform = true
hits = 0
Topic.validates_each(:title, :content, [:title, :content]) do |record, attr|
if perform
record.errors.add attr, 'gotcha'
hits += 1
end
record.errors.add attr, 'gotcha'
hits += 1
end
t = Topic.new("title" => "valid", "content" => "whatever")
assert !t.save
assert_equal 4, hits
assert_equal %w(gotcha gotcha), t.errors.on(:title)
assert_equal %w(gotcha gotcha), t.errors.on(:content)
ensure
perform = false
end
def test_no_title_confirmation
@ -315,8 +306,12 @@ class ValidationsTest < ActiveRecord::TestCase
end
def test_validates_acceptance_of_as_database_column
reply = PlagiarizedReply.create("author_name" => "Dan Brown")
assert_equal "Dan Brown", reply["author_name"]
repair_validations(Reply) do
Reply.validates_acceptance_of(:author_name)
reply = Reply.create("author_name" => "Dan Brown")
assert_equal "Dan Brown", reply["author_name"]
end
end
def test_validates_acceptance_of_with_non_existant_table
@ -372,22 +367,24 @@ class ValidationsTest < ActiveRecord::TestCase
end
def test_validate_uniqueness_with_scope
Reply.validates_uniqueness_of(:content, :scope => "parent_id")
repair_validations(Reply) do
Reply.validates_uniqueness_of(:content, :scope => "parent_id")
t = Topic.create("title" => "I'm unique!")
t = Topic.create("title" => "I'm unique!")
r1 = t.replies.create "title" => "r1", "content" => "hello world"
assert r1.valid?, "Saving r1"
r1 = t.replies.create "title" => "r1", "content" => "hello world"
assert r1.valid?, "Saving r1"
r2 = t.replies.create "title" => "r2", "content" => "hello world"
assert !r2.valid?, "Saving r2 first time"
r2 = t.replies.create "title" => "r2", "content" => "hello world"
assert !r2.valid?, "Saving r2 first time"
r2.content = "something else"
assert r2.save, "Saving r2 second time"
r2.content = "something else"
assert r2.save, "Saving r2 second time"
t2 = Topic.create("title" => "I'm unique too!")
r3 = t2.replies.create "title" => "r3", "content" => "hello world"
assert r3.valid?, "Saving r3"
t2 = Topic.create("title" => "I'm unique too!")
r3 = t2.replies.create "title" => "r3", "content" => "hello world"
assert r3.valid?, "Saving r3"
end
end
def test_validate_uniqueness_scoped_to_defining_class
@ -406,27 +403,29 @@ class ValidationsTest < ActiveRecord::TestCase
end
def test_validate_uniqueness_with_scope_array
Reply.validates_uniqueness_of(:author_name, :scope => [:author_email_address, :parent_id])
repair_validations(Reply) do
Reply.validates_uniqueness_of(:author_name, :scope => [:author_email_address, :parent_id])
t = Topic.create("title" => "The earth is actually flat!")
t = Topic.create("title" => "The earth is actually flat!")
r1 = t.replies.create "author_name" => "jeremy", "author_email_address" => "jeremy@rubyonrails.com", "title" => "You're crazy!", "content" => "Crazy reply"
assert r1.valid?, "Saving r1"
r1 = t.replies.create "author_name" => "jeremy", "author_email_address" => "jeremy@rubyonrails.com", "title" => "You're crazy!", "content" => "Crazy reply"
assert r1.valid?, "Saving r1"
r2 = t.replies.create "author_name" => "jeremy", "author_email_address" => "jeremy@rubyonrails.com", "title" => "You're crazy!", "content" => "Crazy reply again..."
assert !r2.valid?, "Saving r2. Double reply by same author."
r2 = t.replies.create "author_name" => "jeremy", "author_email_address" => "jeremy@rubyonrails.com", "title" => "You're crazy!", "content" => "Crazy reply again..."
assert !r2.valid?, "Saving r2. Double reply by same author."
r2.author_email_address = "jeremy_alt_email@rubyonrails.com"
assert r2.save, "Saving r2 the second time."
r2.author_email_address = "jeremy_alt_email@rubyonrails.com"
assert r2.save, "Saving r2 the second time."
r3 = t.replies.create "author_name" => "jeremy", "author_email_address" => "jeremy_alt_email@rubyonrails.com", "title" => "You're wrong", "content" => "It's cubic"
assert !r3.valid?, "Saving r3"
r3 = t.replies.create "author_name" => "jeremy", "author_email_address" => "jeremy_alt_email@rubyonrails.com", "title" => "You're wrong", "content" => "It's cubic"
assert !r3.valid?, "Saving r3"
r3.author_name = "jj"
assert r3.save, "Saving r3 the second time."
r3.author_name = "jj"
assert r3.save, "Saving r3 the second time."
r3.author_name = "jeremy"
assert !r3.save, "Saving r3 the third time."
r3.author_name = "jeremy"
assert !r3.save, "Saving r3 the third time."
end
end
def test_validate_case_insensitive_uniqueness
@ -523,10 +522,12 @@ class ValidationsTest < ActiveRecord::TestCase
end
def test_validate_uniqueness_with_columns_which_are_sql_keywords
Guid.validates_uniqueness_of :key
g = Guid.new
g.key = "foo"
assert_nothing_raised { !g.valid? }
repair_validations(Guid) do
Guid.validates_uniqueness_of :key
g = Guid.new
g.key = "foo"
assert_nothing_raised { !g.valid? }
end
end
def test_validate_straight_inheritance_uniqueness
@ -648,10 +649,12 @@ class ValidationsTest < ActiveRecord::TestCase
end
def test_numericality_with_getter_method
Developer.validates_numericality_of( :salary )
developer = Developer.new("name" => "michael", "salary" => nil)
developer.instance_eval("def salary; read_attribute('salary') ? read_attribute('salary') : 100000; end")
assert developer.valid?
repair_validations(Developer) do
Developer.validates_numericality_of( :salary )
developer = Developer.new("name" => "michael", "salary" => nil)
developer.instance_eval("def salary; read_attribute('salary') ? read_attribute('salary') : 100000; end")
assert developer.valid?
end
end
def test_validates_length_of_with_allow_nil
@ -684,10 +687,12 @@ class ValidationsTest < ActiveRecord::TestCase
end
def test_numericality_with_allow_nil_and_getter_method
Developer.validates_numericality_of( :salary, :allow_nil => true)
developer = Developer.new("name" => "michael", "salary" => nil)
developer.instance_eval("def salary; read_attribute('salary') ? read_attribute('salary') : 100000; end")
assert developer.valid?
repair_validations(Developer) do
Developer.validates_numericality_of( :salary, :allow_nil => true)
developer = Developer.new("name" => "michael", "salary" => nil)
developer.instance_eval("def salary; read_attribute('salary') ? read_attribute('salary') : 100000; end")
assert developer.valid?
end
end
def test_validates_exclusion_of
@ -892,26 +897,30 @@ class ValidationsTest < ActiveRecord::TestCase
end
def test_validates_size_of_association
assert_nothing_raised { Topic.validates_size_of :replies, :minimum => 1 }
t = Topic.new('title' => 'noreplies', 'content' => 'whatever')
assert !t.save
assert t.errors.on(:replies)
reply = t.replies.build('title' => 'areply', 'content' => 'whateveragain')
assert t.valid?
repair_validations(Owner) do
assert_nothing_raised { Owner.validates_size_of :pets, :minimum => 1 }
o = Owner.new('name' => 'nopets')
assert !o.save
assert o.errors.on(:pets)
pet = o.pets.build('name' => 'apet')
assert o.valid?
end
end
def test_validates_size_of_association_using_within
assert_nothing_raised { Topic.validates_size_of :replies, :within => 1..2 }
t = Topic.new('title' => 'noreplies', 'content' => 'whatever')
assert !t.save
assert t.errors.on(:replies)
repair_validations(Owner) do
assert_nothing_raised { Owner.validates_size_of :pets, :within => 1..2 }
o = Owner.new('name' => 'nopets')
assert !o.save
assert o.errors.on(:pets)
reply = t.replies.build('title' => 'areply', 'content' => 'whateveragain')
assert t.valid?
pet = o.pets.build('name' => 'apet')
assert o.valid?
2.times { t.replies.build('title' => 'areply', 'content' => 'whateveragain') }
assert !t.save
assert t.errors.on(:replies)
2.times { o.pets.build('name' => 'apet') }
assert !o.save
assert o.errors.on(:pets)
end
end
def test_validates_length_of_nasty_params
@ -949,6 +958,19 @@ class ValidationsTest < ActiveRecord::TestCase
assert_equal "boo 5", t.errors["title"]
end
def test_validates_length_of_custom_errors_for_in
Topic.validates_length_of(:title, :in => 10..20, :message => "hoo {{count}}")
t = Topic.create("title" => "uhohuhoh", "content" => "whatever")
assert !t.valid?
assert t.errors.on(:title)
assert_equal "hoo 10", t.errors["title"]
t = Topic.create("title" => "uhohuhohuhohuhohuhohuhohuhohuhoh", "content" => "whatever")
assert !t.valid?
assert t.errors.on(:title)
assert_equal "hoo 20", t.errors["title"]
end
def test_validates_length_of_custom_errors_for_maximum_with_too_long
Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}" )
t = Topic.create("title" => "uhohuhoh", "content" => "whatever")
@ -1102,13 +1124,15 @@ class ValidationsTest < ActiveRecord::TestCase
end
def test_validates_size_of_association_utf8
with_kcode('UTF8') do
assert_nothing_raised { Topic.validates_size_of :replies, :minimum => 1 }
t = Topic.new('title' => 'あいうえお', 'content' => 'かきくけこ')
assert !t.save
assert t.errors.on(:replies)
t.replies.build('title' => 'あいうえお', 'content' => 'かきくけこ')
assert t.valid?
repair_validations(Owner) do
with_kcode('UTF8') do
assert_nothing_raised { Owner.validates_size_of :pets, :minimum => 1 }
o = Owner.new('name' => 'あいうえおかきくけこ')
assert !o.save
assert o.errors.on(:pets)
o.pets.build('name' => 'あいうえおかきくけこ')
assert o.valid?
end
end
end
@ -1127,14 +1151,16 @@ class ValidationsTest < ActiveRecord::TestCase
end
def test_validates_associated_one
Reply.validates_associated( :topic )
Topic.validates_presence_of( :content )
r = Reply.new("title" => "A reply", "content" => "with content!")
r.topic = Topic.create("title" => "uhohuhoh")
assert !r.valid?
assert r.errors.on(:topic)
r.topic.content = "non-empty"
assert r.valid?
repair_validations(Reply) do
Reply.validates_associated( :topic )
Topic.validates_presence_of( :content )
r = Reply.new("title" => "A reply", "content" => "with content!")
r.topic = Topic.create("title" => "uhohuhoh")
assert !r.valid?
assert r.errors.on(:topic)
r.topic.content = "non-empty"
assert r.valid?
end
end
def test_validate_block
@ -1158,85 +1184,105 @@ class ValidationsTest < ActiveRecord::TestCase
end
def test_validates_acceptance_of_with_custom_error_using_quotes
Developer.validates_acceptance_of :salary, :message=> "This string contains 'single' and \"double\" quotes"
d = Developer.new
d.salary = "0"
assert !d.valid?
assert_equal "This string contains 'single' and \"double\" quotes", d.errors.on(:salary).last
repair_validations(Developer) do
Developer.validates_acceptance_of :salary, :message=> "This string contains 'single' and \"double\" quotes"
d = Developer.new
d.salary = "0"
assert !d.valid?
assert_equal "This string contains 'single' and \"double\" quotes", d.errors.on(:salary).last
end
end
def test_validates_confirmation_of_with_custom_error_using_quotes
Developer.validates_confirmation_of :name, :message=> "confirm 'single' and \"double\" quotes"
d = Developer.new
d.name = "John"
d.name_confirmation = "Johnny"
assert !d.valid?
assert_equal "confirm 'single' and \"double\" quotes", d.errors.on(:name)
repair_validations(Developer) do
Developer.validates_confirmation_of :name, :message=> "confirm 'single' and \"double\" quotes"
d = Developer.new
d.name = "John"
d.name_confirmation = "Johnny"
assert !d.valid?
assert_equal "confirm 'single' and \"double\" quotes", d.errors.on(:name)
end
end
def test_validates_format_of_with_custom_error_using_quotes
Developer.validates_format_of :name, :with => /^(A-Z*)$/, :message=> "format 'single' and \"double\" quotes"
d = Developer.new
d.name = d.name_confirmation = "John 32"
assert !d.valid?
assert_equal "format 'single' and \"double\" quotes", d.errors.on(:name)
repair_validations(Developer) do
Developer.validates_format_of :name, :with => /^(A-Z*)$/, :message=> "format 'single' and \"double\" quotes"
d = Developer.new
d.name = d.name_confirmation = "John 32"
assert !d.valid?
assert_equal "format 'single' and \"double\" quotes", d.errors.on(:name)
end
end
def test_validates_inclusion_of_with_custom_error_using_quotes
Developer.validates_inclusion_of :salary, :in => 1000..80000, :message=> "This string contains 'single' and \"double\" quotes"
d = Developer.new
d.salary = "90,000"
assert !d.valid?
assert_equal "This string contains 'single' and \"double\" quotes", d.errors.on(:salary).last
repair_validations(Developer) do
Developer.validates_inclusion_of :salary, :in => 1000..80000, :message=> "This string contains 'single' and \"double\" quotes"
d = Developer.new
d.salary = "90,000"
assert !d.valid?
assert_equal "This string contains 'single' and \"double\" quotes", d.errors.on(:salary).last
end
end
def test_validates_length_of_with_custom_too_long_using_quotes
Developer.validates_length_of :name, :maximum => 4, :too_long=> "This string contains 'single' and \"double\" quotes"
d = Developer.new
d.name = "Jeffrey"
assert !d.valid?
assert_equal "This string contains 'single' and \"double\" quotes", d.errors.on(:name).last
repair_validations(Developer) do
Developer.validates_length_of :name, :maximum => 4, :too_long=> "This string contains 'single' and \"double\" quotes"
d = Developer.new
d.name = "Jeffrey"
assert !d.valid?
assert_equal "This string contains 'single' and \"double\" quotes", d.errors.on(:name)
end
end
def test_validates_length_of_with_custom_too_short_using_quotes
Developer.validates_length_of :name, :minimum => 4, :too_short=> "This string contains 'single' and \"double\" quotes"
d = Developer.new
d.name = "Joe"
assert !d.valid?
assert_equal "This string contains 'single' and \"double\" quotes", d.errors.on(:name).last
repair_validations(Developer) do
Developer.validates_length_of :name, :minimum => 4, :too_short=> "This string contains 'single' and \"double\" quotes"
d = Developer.new
d.name = "Joe"
assert !d.valid?
assert_equal "This string contains 'single' and \"double\" quotes", d.errors.on(:name)
end
end
def test_validates_length_of_with_custom_message_using_quotes
Developer.validates_length_of :name, :minimum => 4, :message=> "This string contains 'single' and \"double\" quotes"
d = Developer.new
d.name = "Joe"
assert !d.valid?
assert_equal "This string contains 'single' and \"double\" quotes", d.errors.on(:name).last
repair_validations(Developer) do
Developer.validates_length_of :name, :minimum => 4, :message=> "This string contains 'single' and \"double\" quotes"
d = Developer.new
d.name = "Joe"
assert !d.valid?
assert_equal "This string contains 'single' and \"double\" quotes", d.errors.on(:name)
end
end
def test_validates_presence_of_with_custom_message_using_quotes
Developer.validates_presence_of :non_existent, :message=> "This string contains 'single' and \"double\" quotes"
d = Developer.new
d.name = "Joe"
assert !d.valid?
assert_equal "This string contains 'single' and \"double\" quotes", d.errors.on(:non_existent)
repair_validations(Developer) do
Developer.validates_presence_of :non_existent, :message=> "This string contains 'single' and \"double\" quotes"
d = Developer.new
d.name = "Joe"
assert !d.valid?
assert_equal "This string contains 'single' and \"double\" quotes", d.errors.on(:non_existent)
end
end
def test_validates_uniqueness_of_with_custom_message_using_quotes
Developer.validates_uniqueness_of :name, :message=> "This string contains 'single' and \"double\" quotes"
d = Developer.new
d.name = "David"
assert !d.valid?
assert_equal "This string contains 'single' and \"double\" quotes", d.errors.on(:name).last
repair_validations(Developer) do
Developer.validates_uniqueness_of :name, :message=> "This string contains 'single' and \"double\" quotes"
d = Developer.new
d.name = "David"
assert !d.valid?
assert_equal "This string contains 'single' and \"double\" quotes", d.errors.on(:name)
end
end
def test_validates_associated_with_custom_message_using_quotes
Reply.validates_associated :topic, :message=> "This string contains 'single' and \"double\" quotes"
Topic.validates_presence_of :content
r = Reply.create("title" => "A reply", "content" => "with content!")
r.topic = Topic.create("title" => "uhohuhoh")
assert !r.valid?
assert_equal "This string contains 'single' and \"double\" quotes", r.errors.on(:topic).last
repair_validations(Reply) do
Reply.validates_associated :topic, :message=> "This string contains 'single' and \"double\" quotes"
Topic.validates_presence_of :content
r = Reply.create("title" => "A reply", "content" => "with content!")
r.topic = Topic.create("title" => "uhohuhoh")
assert !r.valid?
assert_equal "This string contains 'single' and \"double\" quotes", r.errors.on(:topic)
end
end
def test_if_validation_using_method_true
@ -1346,13 +1392,15 @@ class ValidationsTest < ActiveRecord::TestCase
end
def test_validates_associated_missing
Reply.validates_presence_of(:topic)
r = Reply.create("title" => "A reply", "content" => "with content!")
assert !r.valid?
assert r.errors.on(:topic)
repair_validations(Reply) do
Reply.validates_presence_of(:topic)
r = Reply.create("title" => "A reply", "content" => "with content!")
assert !r.valid?
assert r.errors.on(:topic)
r.topic = Topic.find :first
assert r.valid?
r.topic = Topic.find :first
assert r.valid?
end
end
def test_errors_to_xml
@ -1364,14 +1412,14 @@ class ValidationsTest < ActiveRecord::TestCase
assert xml.include?("<error>Content Empty</error>")
end
def test_validation_order
Topic.validates_presence_of :title
Topic.validates_length_of :title, :minimum => 2
def test_validation_order
Topic.validates_presence_of :title
Topic.validates_length_of :title, :minimum => 2
t = Topic.new("title" => "")
assert !t.valid?
assert_equal "can't be blank", t.errors.on("title").first
end
t = Topic.new("title" => "")
assert !t.valid?
assert_equal "can't be blank", t.errors.on("title").first
end
# previous implementation of validates_presence_of eval'd the
# string with the wrong binding, this regression test is to
@ -1423,11 +1471,7 @@ class ValidatesNumericalityTest < ActiveRecord::TestCase
JUNK = ["not a number", "42 not a number", "0xdeadbeef", "00-1", "--3", "+-3", "+3-1", "-+019.0", "12.12.13.12", "123\nnot a number"]
INFINITY = [1.0/0.0]
def setup
Topic.instance_variable_set("@validate_callbacks", ActiveSupport::Callbacks::CallbackChain.new)
Topic.instance_variable_set("@validate_on_create_callbacks", ActiveSupport::Callbacks::CallbackChain.new)
Topic.instance_variable_set("@validate_on_update_callbacks", ActiveSupport::Callbacks::CallbackChain.new)
end
repair_validations(Topic)
def test_default_validates_numericality_of
Topic.validates_numericality_of :approved

View file

@ -31,6 +31,13 @@ class XmlSerializationTest < ActiveRecord::TestCase
assert_match %r{<created_at}, @xml
end
def test_should_allow_camelized_tags
@xml = Contact.new.to_xml :root => 'xml_contact', :camelize => true
assert_match %r{^<XmlContact>}, @xml
assert_match %r{</XmlContact>$}, @xml
assert_match %r{<CreatedAt}, @xml
end
def test_should_include_yielded_additions
@xml = Contact.new.to_xml do |xml|
xml.creator "David"

View file

@ -0,0 +1,18 @@
print "Using Derby via JRuby, activerecord-jdbc-adapter and activerecord-jdbcderby-adapter\n"
require_dependency 'models/course'
require 'logger'
ActiveRecord::Base.logger = Logger.new("debug.log")
ActiveRecord::Base.configurations = {
'arunit' => {
:adapter => 'jdbcderby',
:database => 'activerecord_unittest'
},
'arunit2' => {
:adapter => 'jdbcderby',
:database => 'activerecord_unittest2'
}
}
ActiveRecord::Base.establish_connection 'arunit'
Course.establish_connection 'arunit2'

View file

@ -0,0 +1,18 @@
print "Using H2 via JRuby, activerecord-jdbc-adapter and activerecord-jdbch2-adapter\n"
require_dependency 'models/course'
require 'logger'
ActiveRecord::Base.logger = Logger.new("debug.log")
ActiveRecord::Base.configurations = {
'arunit' => {
:adapter => 'jdbch2',
:database => 'activerecord_unittest'
},
'arunit2' => {
:adapter => 'jdbch2',
:database => 'activerecord_unittest2'
}
}
ActiveRecord::Base.establish_connection 'arunit'
Course.establish_connection 'arunit2'

View file

@ -0,0 +1,18 @@
print "Using HSQLDB via JRuby, activerecord-jdbc-adapter and activerecord-jdbchsqldb-adapter\n"
require_dependency 'models/course'
require 'logger'
ActiveRecord::Base.logger = Logger.new("debug.log")
ActiveRecord::Base.configurations = {
'arunit' => {
:adapter => 'jdbchsqldb',
:database => 'activerecord_unittest'
},
'arunit2' => {
:adapter => 'jdbchsqldb',
:database => 'activerecord_unittest2'
}
}
ActiveRecord::Base.establish_connection 'arunit'
Course.establish_connection 'arunit2'

View file

@ -0,0 +1,26 @@
print "Using MySQL via JRuby, activerecord-jdbc-adapter and activerecord-jdbcmysql-adapter\n"
require_dependency 'models/course'
require 'logger'
ActiveRecord::Base.logger = Logger.new("debug.log")
# GRANT ALL PRIVILEGES ON activerecord_unittest.* to 'rails'@'localhost';
# GRANT ALL PRIVILEGES ON activerecord_unittest2.* to 'rails'@'localhost';
ActiveRecord::Base.configurations = {
'arunit' => {
:adapter => 'jdbcmysql',
:username => 'rails',
:encoding => 'utf8',
:database => 'activerecord_unittest',
},
'arunit2' => {
:adapter => 'jdbcmysql',
:username => 'rails',
:database => 'activerecord_unittest2'
}
}
ActiveRecord::Base.establish_connection 'arunit'
Course.establish_connection 'arunit2'

View file

@ -0,0 +1,26 @@
print "Using Postgrsql via JRuby, activerecord-jdbc-adapter and activerecord-postgresql-adapter\n"
require_dependency 'models/course'
require 'logger'
ActiveRecord::Base.logger = Logger.new("debug.log")
# createuser rails --createdb --no-superuser --no-createrole
# createdb -O rails activerecord_unittest
# createdb -O rails activerecord_unittest2
ActiveRecord::Base.configurations = {
'arunit' => {
:adapter => 'jdbcpostgresql',
:username => ENV['USER'] || 'rails',
:database => 'activerecord_unittest'
},
'arunit2' => {
:adapter => 'jdbcpostgresql',
:username => ENV['USER'] || 'rails',
:database => 'activerecord_unittest2'
}
}
ActiveRecord::Base.establish_connection 'arunit'
Course.establish_connection 'arunit2'

View file

@ -0,0 +1,25 @@
print "Using SQLite3 via JRuby, activerecord-jdbc-adapter and activerecord-jdbcsqlite3-adapter\n"
require_dependency 'models/course'
require 'logger'
ActiveRecord::Base.logger = Logger.new("debug.log")
class SqliteError < StandardError
end
BASE_DIR = FIXTURES_ROOT
sqlite_test_db = "#{BASE_DIR}/fixture_database.sqlite3"
sqlite_test_db2 = "#{BASE_DIR}/fixture_database_2.sqlite3"
def make_connection(clazz, db_file)
ActiveRecord::Base.configurations = { clazz.name => { :adapter => 'jdbcsqlite3', :database => db_file, :timeout => 5000 } }
unless File.exist?(db_file)
puts "SQLite3 database not found at #{db_file}. Rebuilding it."
sqlite_command = %Q{sqlite3 "#{db_file}" "create table a (a integer); drop table a;"}
puts "Executing '#{sqlite_command}'"
raise SqliteError.new("Seems that there is no sqlite3 executable available") unless system(sqlite_command)
end
clazz.establish_connection(clazz.name)
end
make_connection(ActiveRecord::Base, sqlite_test_db)
make_connection(Course, sqlite_test_db2)

View file

@ -0,0 +1,6 @@
founding:
id: 1
name: Founding
provisional:
id: 2
name: Provisional

View file

@ -1,4 +1,6 @@
groucho:
name: Groucho Marx
member_type_id: 1
some_other_guy:
name: Englebert Humperdink
name: Englebert Humperdink
member_type_id: 2

View file

@ -1,6 +1,15 @@
michael:
id: 1
first_name: Michael
primary_contact_id: 2
gender: M
david:
id: 2
first_name: David
first_name: David
primary_contact_id: 3
gender: M
susan:
id: 3
first_name: Susan
primary_contact_id: 2
gender: F

View file

@ -1,6 +1,7 @@
class Author < ActiveRecord::Base
has_many :posts
has_many :posts_with_comments, :include => :comments, :class_name => "Post"
has_many :popular_grouped_posts, :include => :comments, :class_name => "Post", :group => "type", :having => "SUM(comments_count) > 1", :select => "type"
has_many :posts_with_comments_sorted_by_comment_id, :include => :comments, :class_name => "Post", :order => 'comments.id'
has_many :posts_sorted_by_id_limited, :class_name => "Post", :order => 'posts.id', :limit => 1
has_many :posts_with_categories, :include => :categories, :class_name => "Post"

View file

@ -0,0 +1,3 @@
class Bird < ActiveRecord::Base
validates_presence_of :name
end

View file

@ -14,6 +14,7 @@ class Category < ActiveRecord::Base
:class_name => 'Post',
:conditions => { :title => 'Yet Another Testing Title' }
has_and_belongs_to_many :popular_grouped_posts, :class_name => "Post", :group => "posts.type", :having => "sum(comments.post_id) > 2", :include => :comments
has_and_belongs_to_many :posts_gruoped_by_title, :class_name => "Post", :group => "title", :select => "title"
def self.what_are_you

View file

@ -80,6 +80,7 @@ class ExclusivelyDependentFirm < Company
has_one :account, :foreign_key => "firm_id", :dependent => :delete
has_many :dependent_sanitized_conditional_clients_of_firm, :foreign_key => "client_of", :class_name => "Client", :order => "id", :dependent => :delete_all, :conditions => "name = 'BigShot Inc.'"
has_many :dependent_conditional_clients_of_firm, :foreign_key => "client_of", :class_name => "Client", :order => "id", :dependent => :delete_all, :conditions => ["name = ?", 'BigShot Inc.']
has_many :dependent_hash_conditional_clients_of_firm, :foreign_key => "client_of", :class_name => "Client", :order => "id", :dependent => :delete_all, :conditions => {:name => 'BigShot Inc.'}
end
class Client < Company

View file

@ -77,3 +77,15 @@ class DeveloperWithBeforeDestroyRaise < ActiveRecord::Base
raise if projects.empty?
end
end
class DeveloperOrderedBySalary < ActiveRecord::Base
self.table_name = 'developers'
default_scope :order => 'salary DESC'
named_scope :by_name, :order => 'name DESC'
def self.all_ordered_by_name
with_scope(:find => { :order => 'name DESC' }) do
find(:all)
end
end
end

View file

@ -8,4 +8,5 @@ class Member < ActiveRecord::Base
has_one :sponsor_club, :through => :sponsor
has_one :member_detail
has_one :organization, :through => :member_detail
belongs_to :member_type
end

View file

@ -1,4 +1,5 @@
class MemberDetail < ActiveRecord::Base
belongs_to :member
belongs_to :organization
has_one :member_type, :through => :member
end

View file

@ -0,0 +1,3 @@
class MemberType < ActiveRecord::Base
has_many :members
end

View file

@ -4,6 +4,8 @@ class Parrot < ActiveRecord::Base
has_and_belongs_to_many :treasures
has_many :loots, :as => :looter
alias_attribute :title, :name
validates_presence_of :name
end
class LiveParrot < Parrot

View file

@ -7,4 +7,10 @@ class Person < ActiveRecord::Base
has_many :jobs, :through => :references
has_one :favourite_reference, :class_name => 'Reference', :conditions => ['favourite=?', true]
has_many :posts_with_comments_sorted_by_comment_id, :through => :readers, :source => :post, :include => :comments, :order => 'comments.id'
belongs_to :primary_contact, :class_name => 'Person'
has_many :agents, :class_name => 'Person', :foreign_key => 'primary_contact_id'
named_scope :males, :conditions => { :gender => 'M' }
named_scope :females, :conditions => { :gender => 'F' }
end

View file

@ -5,5 +5,12 @@ class Pirate < ActiveRecord::Base
has_many :treasure_estimates, :through => :treasures, :source => :price_estimates
# These both have :autosave enabled because accepts_nested_attributes_for is used on them.
has_one :ship
has_many :birds
accepts_nested_attributes_for :parrots, :birds, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? }
accepts_nested_attributes_for :ship, :allow_destroy => true
validates_presence_of :catchphrase
end

View file

@ -17,6 +17,12 @@ class Post < ActiveRecord::Base
has_one :last_comment, :class_name => 'Comment', :order => 'id desc'
named_scope :with_special_comments, :joins => :comments, :conditions => {:comments => {:type => 'SpecialComment'} }
named_scope :with_very_special_comments, :joins => :comments, :conditions => {:comments => {:type => 'VerySpecialComment'} }
named_scope :with_post, lambda {|post_id|
{ :joins => :comments, :conditions => {:comments => {:post_id => post_id} } }
}
has_many :comments, :order => "body" do
def find_most_recent
find(:first, :order => "id DESC")

View file

@ -13,6 +13,7 @@ class Project < ActiveRecord::Base
:after_add => Proc.new {|o, r| o.developers_log << "after_adding#{r.id || '<new>'}"},
:before_remove => Proc.new {|o, r| o.developers_log << "before_removing#{r.id}"},
:after_remove => Proc.new {|o, r| o.developers_log << "after_removing#{r.id}"}
has_and_belongs_to_many :well_payed_salary_groups, :class_name => "Developer", :group => "salary", :having => "SUM(salary) > 10000", :select => "SUM(salary) as salary"
attr_accessor :developers_log

View file

@ -1,3 +1,10 @@
class Ship < ActiveRecord::Base
self.record_timestamps = false
belongs_to :pirate
has_many :parts, :class_name => 'ShipPart', :autosave => true
accepts_nested_attributes_for :pirate, :allow_destroy => true
validates_presence_of :name
end

View file

@ -0,0 +1,5 @@
class ShipPart < ActiveRecord::Base
belongs_to :ship
validates_presence_of :name
end

View file

@ -4,6 +4,8 @@ class Topic < ActiveRecord::Base
{ :conditions => ['written_on < ?', time] }
}
named_scope :approved, :conditions => {:approved => true}
named_scope :rejected, :conditions => {:approved => false}
named_scope :by_lifo, :conditions => {:author_name => 'lifo'}
named_scope :approved_as_hash_condition, :conditions => {:topics => {:approved => true}}

View file

@ -55,6 +55,11 @@ ActiveRecord::Schema.define do
t.binary :data
end
create_table :birds, :force => true do |t|
t.string :name
t.integer :pirate_id
end
create_table :books, :force => true do |t|
t.column :name, :string
end
@ -154,6 +159,11 @@ ActiveRecord::Schema.define do
t.string :name
end
create_table :goofy_string_id, :force => true, :id => false do |t|
t.string :id, :null => false
t.string :info
end
create_table :items, :force => true do |t|
t.column :name, :integer
end
@ -195,6 +205,7 @@ ActiveRecord::Schema.define do
create_table :members, :force => true do |t|
t.string :name
t.integer :member_type_id
end
create_table :member_details, :force => true do |t|
@ -210,6 +221,10 @@ ActiveRecord::Schema.define do
t.string :type
end
create_table :member_types, :force => true do |t|
t.string :name
end
create_table :references, :force => true do |t|
t.integer :person_id
t.integer :job_id
@ -247,6 +262,7 @@ ActiveRecord::Schema.define do
t.decimal :world_population, :precision => 10, :scale => 0
t.decimal :my_house_population, :precision => 2, :scale => 0
t.decimal :decimal_number_with_default, :precision => 3, :scale => 2, :default => 2.78
t.float :temperature
end
create_table :orders, :force => true do |t|
@ -293,8 +309,10 @@ ActiveRecord::Schema.define do
end
create_table :people, :force => true do |t|
t.string :first_name, :null => false
t.integer :lock_version, :null => false, :default => 0
t.string :first_name, :null => false
t.references :primary_contact
t.string :gender, :limit => 1
t.integer :lock_version, :null => false, :default => 0
end
create_table :pets, :primary_key => :pet_id ,:force => true do |t|
@ -343,12 +361,18 @@ ActiveRecord::Schema.define do
create_table :ships, :force => true do |t|
t.string :name
t.integer :pirate_id
t.datetime :created_at
t.datetime :created_on
t.datetime :updated_at
t.datetime :updated_on
end
create_table :ship_parts, :force => true do |t|
t.string :name
t.integer :ship_id
end
create_table :sponsors, :force => true do |t|
t.integer :club_id
t.integer :sponsorable_id