unfreeze rails

This commit is contained in:
Reinier Balt 2008-11-28 08:16:04 +01:00
parent bd2b410c7b
commit fe5f962dcf
1493 changed files with 1 additions and 191145 deletions

File diff suppressed because it is too large Load diff

View file

@ -1,20 +0,0 @@
Copyright (c) 2004-2008 David Heinemeier Hansson
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -1,351 +0,0 @@
= Active Record -- Object-relation mapping put on rails
Active Record connects business objects and database tables to create a persistable
domain model where logic and data are presented in one wrapping. It's an implementation
of the object-relational mapping (ORM) pattern[http://www.martinfowler.com/eaaCatalog/activeRecord.html]
by the same name as described by Martin Fowler:
"An object that wraps a row in a database table or view, encapsulates
the database access, and adds domain logic on that data."
Active Record's main contribution to the pattern is to relieve the original of two stunting problems:
lack of associations and inheritance. By adding a simple domain language-like set of macros to describe
the former and integrating the Single Table Inheritance pattern for the latter, Active Record narrows the
gap of functionality between the data mapper and active record approach.
A short rundown of the major features:
* Automated mapping between classes and tables, attributes and columns.
class Product < ActiveRecord::Base; end
...is automatically mapped to the table named "products", such as:
CREATE TABLE products (
id int(11) NOT NULL auto_increment,
name varchar(255),
PRIMARY KEY (id)
);
...which again gives Product#name and Product#name=(new_name)
{Learn more}[link:classes/ActiveRecord/Base.html]
* Associations between objects controlled by simple meta-programming macros.
class Firm < ActiveRecord::Base
has_many :clients
has_one :account
belongs_to :conglomorate
end
{Learn more}[link:classes/ActiveRecord/Associations/ClassMethods.html]
* Aggregations of value objects controlled by simple meta-programming macros.
class Account < ActiveRecord::Base
composed_of :balance, :class_name => "Money",
:mapping => %w(balance amount)
composed_of :address,
:mapping => [%w(address_street street), %w(address_city city)]
end
{Learn more}[link:classes/ActiveRecord/Aggregations/ClassMethods.html]
* Validation rules that can differ for new or existing objects.
class Account < ActiveRecord::Base
validates_presence_of :subdomain, :name, :email_address, :password
validates_uniqueness_of :subdomain
validates_acceptance_of :terms_of_service, :on => :create
validates_confirmation_of :password, :email_address, :on => :create
end
{Learn more}[link:classes/ActiveRecord/Validations.html]
* Callbacks as methods or queues on the entire lifecycle (instantiation, saving, destroying, validating, etc).
class Person < ActiveRecord::Base
def before_destroy # is called just before Person#destroy
CreditCard.find(credit_card_id).destroy
end
end
class Account < ActiveRecord::Base
after_find :eager_load, 'self.class.announce(#{id})'
end
{Learn more}[link:classes/ActiveRecord/Callbacks.html]
* Observers for the entire lifecycle
class CommentObserver < ActiveRecord::Observer
def after_create(comment) # is called just after Comment#save
Notifications.deliver_new_comment("david@loudthinking.com", comment)
end
end
{Learn more}[link:classes/ActiveRecord/Observer.html]
* Inheritance hierarchies
class Company < ActiveRecord::Base; end
class Firm < Company; end
class Client < Company; end
class PriorityClient < Client; end
{Learn more}[link:classes/ActiveRecord/Base.html]
* Transactions
# Database transaction
Account.transaction do
david.withdrawal(100)
mary.deposit(100)
end
{Learn more}[link:classes/ActiveRecord/Transactions/ClassMethods.html]
* Reflections on columns, associations, and aggregations
reflection = Firm.reflect_on_association(:clients)
reflection.klass # => Client (class)
Firm.columns # Returns an array of column descriptors for the firms table
{Learn more}[link:classes/ActiveRecord/Reflection/ClassMethods.html]
* Direct manipulation (instead of service invocation)
So instead of (Hibernate[http://www.hibernate.org/] example):
long pkId = 1234;
DomesticCat pk = (DomesticCat) sess.load( Cat.class, new Long(pkId) );
// something interesting involving a cat...
sess.save(cat);
sess.flush(); // force the SQL INSERT
Active Record lets you:
pkId = 1234
cat = Cat.find(pkId)
# something even more interesting involving the same cat...
cat.save
{Learn more}[link:classes/ActiveRecord/Base.html]
* Database abstraction through simple adapters (~100 lines) with a shared connector
ActiveRecord::Base.establish_connection(:adapter => "sqlite", :database => "dbfile")
ActiveRecord::Base.establish_connection(
:adapter => "mysql",
:host => "localhost",
:username => "me",
:password => "secret",
:database => "activerecord"
)
{Learn more}[link:classes/ActiveRecord/Base.html#M000081] and read about the built-in support for
MySQL[link:classes/ActiveRecord/ConnectionAdapters/MysqlAdapter.html], PostgreSQL[link:classes/ActiveRecord/ConnectionAdapters/PostgreSQLAdapter.html], SQLite[link:classes/ActiveRecord/ConnectionAdapters/SQLiteAdapter.html], Oracle[link:classes/ActiveRecord/ConnectionAdapters/OracleAdapter.html], SQLServer[link:classes/ActiveRecord/ConnectionAdapters/SQLServerAdapter.html], and DB2[link:classes/ActiveRecord/ConnectionAdapters/DB2Adapter.html].
* Logging support for Log4r[http://log4r.sourceforge.net] and Logger[http://www.ruby-doc.org/stdlib/libdoc/logger/rdoc]
ActiveRecord::Base.logger = Logger.new(STDOUT)
ActiveRecord::Base.logger = Log4r::Logger.new("Application Log")
* Database agnostic schema management with Migrations
class AddSystemSettings < ActiveRecord::Migration
def self.up
create_table :system_settings do |t|
t.string :name
t.string :label
t.text :value
t.string :type
t.integer :position
end
SystemSetting.create :name => "notice", :label => "Use notice?", :value => 1
end
def self.down
drop_table :system_settings
end
end
{Learn more}[link:classes/ActiveRecord/Migration.html]
== Simple example (1/2): Defining tables and classes (using MySQL)
Data definitions are specified only in the database. Active Record queries the database for
the column names (that then serves to determine which attributes are valid) on regular
object instantiation through the new constructor and relies on the column names in the rows
with the finders.
# CREATE TABLE companies (
# id int(11) unsigned NOT NULL auto_increment,
# client_of int(11),
# name varchar(255),
# type varchar(100),
# PRIMARY KEY (id)
# )
Active Record automatically links the "Company" object to the "companies" table
class Company < ActiveRecord::Base
has_many :people, :class_name => "Person"
end
class Firm < Company
has_many :clients
def people_with_all_clients
clients.inject([]) { |people, client| people + client.people }
end
end
The foreign_key is only necessary because we didn't use "firm_id" in the data definition
class Client < Company
belongs_to :firm, :foreign_key => "client_of"
end
# CREATE TABLE people (
# id int(11) unsigned NOT NULL auto_increment,
# name text,
# company_id text,
# PRIMARY KEY (id)
# )
Active Record will also automatically link the "Person" object to the "people" table
class Person < ActiveRecord::Base
belongs_to :company
end
== Simple example (2/2): Using the domain
Picking a database connection for all the Active Records
ActiveRecord::Base.establish_connection(
:adapter => "mysql",
:host => "localhost",
:username => "me",
:password => "secret",
:database => "activerecord"
)
Create some fixtures
firm = Firm.new("name" => "Next Angle")
# SQL: INSERT INTO companies (name, type) VALUES("Next Angle", "Firm")
firm.save
client = Client.new("name" => "37signals", "client_of" => firm.id)
# SQL: INSERT INTO companies (name, client_of, type) VALUES("37signals", 1, "Firm")
client.save
Lots of different finders
# SQL: SELECT * FROM companies WHERE id = 1
next_angle = Company.find(1)
# SQL: SELECT * FROM companies WHERE id = 1 AND type = 'Firm'
next_angle = Firm.find(1)
# SQL: SELECT * FROM companies WHERE id = 1 AND name = 'Next Angle'
next_angle = Company.find(:first, :conditions => "name = 'Next Angle'")
next_angle = Firm.find_by_sql("SELECT * FROM companies WHERE id = 1").first
The supertype, Company, will return subtype instances
Firm === next_angle
All the dynamic methods added by the has_many macro
next_angle.clients.empty? # true
next_angle.clients.size # total number of clients
all_clients = next_angle.clients
Constrained finds makes access security easier when ID comes from a web-app
# SQL: SELECT * FROM companies WHERE client_of = 1 AND type = 'Client' AND id = 2
thirty_seven_signals = next_angle.clients.find(2)
Bi-directional associations thanks to the "belongs_to" macro
thirty_seven_signals.firm.nil? # true
== Philosophy
Active Record attempts to provide a coherent wrapper as a solution for the inconvenience that is
object-relational mapping. The prime directive for this mapping has been to minimize
the amount of code needed to build a real-world domain model. This is made possible
by relying on a number of conventions that make it easy for Active Record to infer
complex relations and structures from a minimal amount of explicit direction.
Convention over Configuration:
* No XML-files!
* Lots of reflection and run-time extension
* Magic is not inherently a bad word
Admit the Database:
* Lets you drop down to SQL for odd cases and performance
* Doesn't attempt to duplicate or replace data definitions
== Download
The latest version of Active Record can be found at
* http://rubyforge.org/project/showfiles.php?group_id=182
Documentation can be found at
* http://ar.rubyonrails.com
== Installation
The prefered method of installing Active Record is through its GEM file. You'll need to have
RubyGems[http://rubygems.rubyforge.org/wiki/wiki.pl] installed for that, though. If you have,
then use:
% [sudo] gem install activerecord-1.10.0.gem
You can also install Active Record the old-fashioned way with the following command:
% [sudo] ruby install.rb
from its distribution directory.
== License
Active Record is released under the MIT license.
== Support
The Active Record homepage is http://www.rubyonrails.com. You can find the Active Record
RubyForge page at http://rubyforge.org/projects/activerecord. And as Jim from Rake says:
Feel free to submit commits or feature requests. If you send a patch,
remember to update the corresponding unit tests. If fact, I prefer
new feature to be submitted in the form of new unit tests.
For other information, feel free to ask on the rubyonrails-talk
(http://groups.google.com/group/rubyonrails-talk) mailing list.

View file

@ -1,36 +0,0 @@
== Creating the test database
The default names for the test databases are "activerecord_unittest" and
"activerecord_unittest2". If you want to use another database name then be sure
to update the connection adapter setups you want to test with in
test/connections/<your database>/connection.rb.
When you have the database online, you can import the fixture tables with
the test/schema/*.sql files.
Make sure that you create database objects with the same user that you specified in
connection.rb otherwise (on Postgres, at least) tests for default values will fail.
== Running with Rake
The easiest way to run the unit tests is through Rake. The default task runs
the entire test suite for all the adapters. You can also run the suite on just
one adapter by using the tasks test_mysql, test_sqlite, test_postgresql or any
of the other test_ tasks. For more information, checkout the full array of rake
tasks with "rake -T"
Rake can be found at http://rake.rubyforge.org
== Running by hand
Unit tests are located in test/cases directory. If you only want to run a single test suite,
you can do so with:
rake test_mysql TEST=test/cases/base_test.rb
That'll run the base suite using the MySQL-Ruby adapter. Some tests rely on the schema
being initialized - you can initialize the schema with:
rake test_mysql TEST=test/cases/aaa_create_tables_test.rb

View file

@ -1,247 +0,0 @@
require 'rubygems'
require 'rake'
require 'rake/testtask'
require 'rake/rdoctask'
require 'rake/packagetask'
require 'rake/gempackagetask'
require 'rake/contrib/sshpublisher'
require File.join(File.dirname(__FILE__), 'lib', 'active_record', 'version')
require File.expand_path(File.dirname(__FILE__)) + "/test/config"
PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : ''
PKG_NAME = 'activerecord'
PKG_VERSION = ActiveRecord::VERSION::STRING + PKG_BUILD
PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
RELEASE_NAME = "REL #{PKG_VERSION}"
RUBY_FORGE_PROJECT = "activerecord"
RUBY_FORGE_USER = "webster132"
MYSQL_DB_USER = 'rails'
PKG_FILES = FileList[
"lib/**/*", "test/**/*", "examples/**/*", "doc/**/*", "[A-Z]*", "install.rb", "Rakefile"
].exclude(/\bCVS\b|~$/)
desc 'Run mysql, sqlite, and postgresql tests by default'
task :default => :test
desc 'Run mysql, sqlite, and postgresql tests'
task :test => %w(test_mysql test_sqlite test_sqlite3 test_postgresql)
for adapter in %w( mysql postgresql sqlite sqlite3 firebird db2 oracle sybase openbase frontbase )
Rake::TestTask.new("test_#{adapter}") { |t|
t.libs << "test" << "test/connections/native_#{adapter}"
adapter_short = adapter == 'db2' ? adapter : adapter[/^[a-z]+/]
t.test_files=Dir.glob( "test/cases/**/*_test{,_#{adapter_short}}.rb" ).sort
t.verbose = true
}
namespace adapter do
task :test => "test_#{adapter}"
end
end
namespace :mysql do
desc 'Build the MySQL test databases'
task :build_databases do
%x( mysqladmin --user=#{MYSQL_DB_USER} create activerecord_unittest )
%x( mysqladmin --user=#{MYSQL_DB_USER} create activerecord_unittest2 )
end
desc 'Drop the MySQL test databases'
task :drop_databases do
%x( mysqladmin --user=#{MYSQL_DB_USER} -f drop activerecord_unittest )
%x( mysqladmin --user=#{MYSQL_DB_USER} -f drop activerecord_unittest2 )
end
desc 'Rebuild the MySQL test databases'
task :rebuild_databases => [:drop_databases, :build_databases]
end
task :build_mysql_databases => 'mysql:build_databases'
task :drop_mysql_databases => 'mysql:drop_databases'
task :rebuild_mysql_databases => 'mysql:rebuild_databases'
namespace :postgresql do
desc 'Build the PostgreSQL test databases'
task :build_databases do
%x( createdb activerecord_unittest )
%x( createdb activerecord_unittest2 )
end
desc 'Drop the PostgreSQL test databases'
task :drop_databases do
%x( dropdb activerecord_unittest )
%x( dropdb activerecord_unittest2 )
end
desc 'Rebuild the PostgreSQL test databases'
task :rebuild_databases => [:drop_databases, :build_databases]
end
task :build_postgresql_databases => 'postgresql:build_databases'
task :drop_postgresql_databases => 'postgresql:drop_databases'
task :rebuild_postgresql_databases => 'postgresql:rebuild_databases'
namespace :frontbase do
desc 'Build the FrontBase test databases'
task :build_databases => :rebuild_frontbase_databases
desc 'Rebuild the FrontBase test databases'
task :rebuild_databases do
build_frontbase_database = Proc.new do |db_name, sql_definition_file|
%(
STOP DATABASE #{db_name};
DELETE DATABASE #{db_name};
CREATE DATABASE #{db_name};
CONNECT TO #{db_name} AS SESSION_NAME USER _SYSTEM;
SET COMMIT FALSE;
CREATE USER RAILS;
CREATE SCHEMA RAILS AUTHORIZATION RAILS;
COMMIT;
SET SESSION AUTHORIZATION RAILS;
SCRIPT '#{sql_definition_file}';
COMMIT;
DISCONNECT ALL;
)
end
create_activerecord_unittest = build_frontbase_database['activerecord_unittest', File.join(SCHEMA_ROOT, 'frontbase.sql')]
create_activerecord_unittest2 = build_frontbase_database['activerecord_unittest2', File.join(SCHEMA_ROOT, 'frontbase2.sql')]
execute_frontbase_sql = Proc.new do |sql|
system(<<-SHELL)
/Library/FrontBase/bin/sql92 <<-SQL
#{sql}
SQL
SHELL
end
execute_frontbase_sql[create_activerecord_unittest]
execute_frontbase_sql[create_activerecord_unittest2]
end
end
task :build_frontbase_databases => 'frontbase:build_databases'
task :rebuild_frontbase_databases => 'frontbase:rebuild_databases'
# Generate the RDoc documentation
Rake::RDocTask.new { |rdoc|
rdoc.rdoc_dir = 'doc'
rdoc.title = "Active Record -- Object-relation mapping put on rails"
rdoc.options << '--line-numbers' << '--inline-source' << '-A cattr_accessor=object'
rdoc.options << '--charset' << 'utf-8'
rdoc.template = "#{ENV['template']}.rb" if ENV['template']
rdoc.rdoc_files.include('README', 'RUNNING_UNIT_TESTS', 'CHANGELOG')
rdoc.rdoc_files.include('lib/**/*.rb')
rdoc.rdoc_files.exclude('lib/active_record/vendor/*')
rdoc.rdoc_files.include('dev-utils/*.rb')
}
# Enhance rdoc task to copy referenced images also
task :rdoc do
FileUtils.mkdir_p "doc/files/examples/"
FileUtils.copy "examples/associations.png", "doc/files/examples/associations.png"
end
# Create compressed packages
dist_dirs = [ "lib", "test", "examples" ]
spec = Gem::Specification.new do |s|
s.platform = Gem::Platform::RUBY
s.name = PKG_NAME
s.version = PKG_VERSION
s.summary = "Implements the ActiveRecord pattern for ORM."
s.description = %q{Implements the ActiveRecord pattern (Fowler, PoEAA) for ORM. It ties database tables and classes together for business objects, like Customer or Subscription, that can find, save, and destroy themselves without resorting to manual SQL.}
s.files = [ "Rakefile", "install.rb", "README", "RUNNING_UNIT_TESTS", "CHANGELOG" ]
dist_dirs.each do |dir|
s.files = s.files + Dir.glob( "#{dir}/**/*" ).delete_if { |item| item.include?( "\.svn" ) }
end
s.add_dependency('activesupport', '= 2.1.0' + PKG_BUILD)
s.files.delete FIXTURES_ROOT + "/fixture_database.sqlite"
s.files.delete FIXTURES_ROOT + "/fixture_database_2.sqlite"
s.files.delete FIXTURES_ROOT + "/fixture_database.sqlite3"
s.files.delete FIXTURES_ROOT + "/fixture_database_2.sqlite3"
s.require_path = 'lib'
s.autorequire = 'active_record'
s.has_rdoc = true
s.extra_rdoc_files = %w( README )
s.rdoc_options.concat ['--main', 'README']
s.author = "David Heinemeier Hansson"
s.email = "david@loudthinking.com"
s.homepage = "http://www.rubyonrails.org"
s.rubyforge_project = "activerecord"
end
Rake::GemPackageTask.new(spec) do |p|
p.gem_spec = spec
p.need_tar = true
p.need_zip = true
end
task :lines do
lines, codelines, total_lines, total_codelines = 0, 0, 0, 0
for file_name in FileList["lib/active_record/**/*.rb"]
next if file_name =~ /vendor/
f = File.open(file_name)
while line = f.gets
lines += 1
next if line =~ /^\s*$/
next if line =~ /^\s*#/
codelines += 1
end
puts "L: #{sprintf("%4d", lines)}, LOC #{sprintf("%4d", codelines)} | #{file_name}"
total_lines += lines
total_codelines += codelines
lines, codelines = 0, 0
end
puts "Total: Lines #{total_lines}, LOC #{total_codelines}"
end
# Publishing ------------------------------------------------------
desc "Publish the beta gem"
task :pgem => [:package] do
Rake::SshFilePublisher.new("davidhh@wrath.rubyonrails.org", "public_html/gems/gems", "pkg", "#{PKG_FILE_NAME}.gem").upload
`ssh davidhh@wrath.rubyonrails.org './gemupdate.sh'`
end
desc "Publish the API documentation"
task :pdoc => [:rdoc] do
Rake::SshDirPublisher.new("davidhh@wrath.rubyonrails.org", "public_html/ar", "doc").upload
end
desc "Publish the release files to RubyForge."
task :release => [ :package ] do
require 'rubyforge'
require 'rake/contrib/rubyforgepublisher'
packages = %w( gem tgz zip ).collect{ |ext| "pkg/#{PKG_NAME}-#{PKG_VERSION}.#{ext}" }
rubyforge = RubyForge.new
rubyforge.login
rubyforge.add_release(PKG_NAME, PKG_NAME, "REL #{PKG_VERSION}", *packages)
end

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

View file

@ -1,30 +0,0 @@
require 'rbconfig'
require 'find'
require 'ftools'
include Config
# this was adapted from rdoc's install.rb by ways of Log4r
$sitedir = CONFIG["sitelibdir"]
unless $sitedir
version = CONFIG["MAJOR"] + "." + CONFIG["MINOR"]
$libdir = File.join(CONFIG["libdir"], "ruby", version)
$sitedir = $:.find {|x| x =~ /site_ruby/ }
if !$sitedir
$sitedir = File.join($libdir, "site_ruby")
elsif $sitedir !~ Regexp.quote(version)
$sitedir = File.join($sitedir, version)
end
end
# the actual gruntwork
Dir.chdir("lib")
Find.find("active_record", "active_record.rb") { |f|
if f[-3..-1] == ".rb"
File::install(f, File.join($sitedir, *f.split(/\//)), 0644, true)
else
File::makedirs(File.join($sitedir, *f.split(/\//)))
end
}

View file

@ -1,82 +0,0 @@
#--
# Copyright (c) 2004-2008 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#++
$:.unshift(File.dirname(__FILE__)) unless
$:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
unless defined? ActiveSupport
active_support_path = File.dirname(__FILE__) + "/../../activesupport/lib"
if File.exist?(active_support_path)
$:.unshift active_support_path
require 'active_support'
else
require 'rubygems'
gem 'activesupport'
require 'active_support'
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'
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
end
require 'active_record/connection_adapters/abstract_adapter'
require 'active_record/schema_dumper'

View file

@ -1,189 +0,0 @@
module ActiveRecord
module Aggregations # :nodoc:
def self.included(base)
base.extend(ClassMethods)
end
def clear_aggregation_cache #:nodoc:
self.class.reflect_on_all_aggregations.to_a.each do |assoc|
instance_variable_set "@#{assoc.name}", nil
end unless self.new_record?
end
# Active Record implements aggregation through a macro-like class method called +composed_of+ for representing attributes
# as value objects. It expresses relationships like "Account [is] composed of Money [among other things]" or "Person [is]
# composed of [an] address". Each call to the macro adds a description of how the value objects are created from the
# attributes of the entity object (when the entity is initialized either as a new object or from finding an existing object)
# and how it can be turned back into attributes (when the entity is saved to the database). Example:
#
# class Customer < ActiveRecord::Base
# composed_of :balance, :class_name => "Money", :mapping => %w(balance amount)
# composed_of :address, :mapping => [ %w(address_street street), %w(address_city city) ]
# end
#
# The customer class now has the following methods to manipulate the value objects:
# * <tt>Customer#balance, Customer#balance=(money)</tt>
# * <tt>Customer#address, Customer#address=(address)</tt>
#
# These methods will operate with value objects like the ones described below:
#
# class Money
# include Comparable
# attr_reader :amount, :currency
# EXCHANGE_RATES = { "USD_TO_DKK" => 6 }
#
# def initialize(amount, currency = "USD")
# @amount, @currency = amount, currency
# end
#
# def exchange_to(other_currency)
# exchanged_amount = (amount * EXCHANGE_RATES["#{currency}_TO_#{other_currency}"]).floor
# Money.new(exchanged_amount, other_currency)
# end
#
# def ==(other_money)
# amount == other_money.amount && currency == other_money.currency
# end
#
# def <=>(other_money)
# if currency == other_money.currency
# amount <=> amount
# else
# amount <=> other_money.exchange_to(currency).amount
# end
# end
# end
#
# class Address
# attr_reader :street, :city
# def initialize(street, city)
# @street, @city = street, city
# end
#
# def close_to?(other_address)
# city == other_address.city
# end
#
# def ==(other_address)
# city == other_address.city && street == other_address.street
# end
# end
#
# Now it's possible to access attributes from the database through the value objects instead. If you choose to name the
# composition the same as the attribute's name, it will be the only way to access that attribute. That's the case with our
# +balance+ attribute. You interact with the value objects just like you would any other attribute, though:
#
# customer.balance = Money.new(20) # sets the Money value object and the attribute
# customer.balance # => Money value object
# customer.balance.exchanged_to("DKK") # => Money.new(120, "DKK")
# customer.balance > Money.new(10) # => true
# customer.balance == Money.new(20) # => true
# customer.balance < Money.new(5) # => false
#
# Value objects can also be composed of multiple attributes, such as the case of Address. The order of the mappings will
# determine the order of the parameters. Example:
#
# customer.address_street = "Hyancintvej"
# customer.address_city = "Copenhagen"
# customer.address # => Address.new("Hyancintvej", "Copenhagen")
# customer.address = Address.new("May Street", "Chicago")
# customer.address_street # => "May Street"
# customer.address_city # => "Chicago"
#
# == Writing value objects
#
# Value objects are immutable and interchangeable objects that represent a given value, such as a Money object representing
# $5. Two Money objects both representing $5 should be equal (through methods such as <tt>==</tt> and <tt><=></tt> from Comparable if ranking
# makes sense). This is unlike entity objects where equality is determined by identity. An entity class such as Customer can
# easily have two different objects that both have an address on Hyancintvej. Entity identity is determined by object or
# relational unique identifiers (such as primary keys). Normal ActiveRecord::Base classes are entity objects.
#
# It's also important to treat the value objects as immutable. Don't allow the Money object to have its amount changed after
# creation. Create a new Money object with the new value instead. This is exemplified by the Money#exchanged_to method that
# returns a new value object instead of changing its own values. Active Record won't persist value objects that have been
# changed through means other than the writer method.
#
# The immutable requirement is enforced by Active Record by freezing any object assigned as a value object. Attempting to
# change it afterwards will result in a ActiveSupport::FrozenObjectError.
#
# Read more about value objects on http://c2.com/cgi/wiki?ValueObject and on the dangers of not keeping value objects
# immutable on http://c2.com/cgi/wiki?ValueObjectsShouldBeImmutable
#
# == Finding records by a value object
#
# Once a +composed_of+ relationship is specified for a model, records can be loaded from the database by specifying an instance
# of the value object in the conditions hash. The following example finds all customers with +balance_amount+ equal to 20 and
# +balance_currency+ equal to "USD":
#
# Customer.find(:all, :conditions => {:balance => Money.new(20, "USD")})
#
module ClassMethods
# Adds reader and writer methods for manipulating a value object:
# <tt>composed_of :address</tt> adds <tt>address</tt> and <tt>address=(new_address)</tt> methods.
#
# Options are:
# * <tt>:class_name</tt> - specify the class name of the association. Use it only if that name can't be inferred
# from the part id. So <tt>composed_of :address</tt> will by default be linked to the Address class, but
# if the real class name is CompanyAddress, you'll have to specify it with this option.
# * <tt>:mapping</tt> - specifies a number of mapping arrays (attribute, parameter) that bind an attribute name
# to a constructor parameter on the value class.
# * <tt>:allow_nil</tt> - specifies that the aggregate object will not be instantiated when all mapped
# attributes are +nil+. Setting the aggregate class to +nil+ has the effect of writing +nil+ to all mapped attributes.
# This defaults to +false+.
#
# An optional block can be passed to convert the argument that is passed to the writer method into an instance of
# <tt>:class_name</tt>. The block will only be called if the argument is not already an instance of <tt>:class_name</tt>.
#
# Option examples:
# composed_of :temperature, :mapping => %w(reading celsius)
# composed_of(:balance, :class_name => "Money", :mapping => %w(balance amount)) {|balance| balance.to_money }
# composed_of :address, :mapping => [ %w(address_street street), %w(address_city city) ]
# composed_of :gps_location
# composed_of :gps_location, :allow_nil => true
#
def composed_of(part_id, options = {}, &block)
options.assert_valid_keys(:class_name, :mapping, :allow_nil)
name = part_id.id2name
class_name = options[:class_name] || name.camelize
mapping = options[:mapping] || [ name, name ]
mapping = [ mapping ] unless mapping.first.is_a?(Array)
allow_nil = options[:allow_nil] || false
reader_method(name, class_name, mapping, allow_nil)
writer_method(name, class_name, mapping, allow_nil, block)
create_reflection(:composed_of, part_id, options, self)
end
private
def reader_method(name, class_name, mapping, allow_nil)
module_eval do
define_method(name) do |*args|
force_reload = args.first || false
if (instance_variable_get("@#{name}").nil? || force_reload) && (!allow_nil || mapping.any? {|pair| !read_attribute(pair.first).nil? })
instance_variable_set("@#{name}", class_name.constantize.new(*mapping.collect {|pair| read_attribute(pair.first)}))
end
instance_variable_get("@#{name}")
end
end
end
def writer_method(name, class_name, mapping, allow_nil, conversion)
module_eval do
define_method("#{name}=") do |part|
if part.nil? && allow_nil
mapping.each { |pair| self[pair.first] = nil }
instance_variable_set("@#{name}", nil)
else
part = conversion.call(part) unless part.is_a?(class_name.constantize) || conversion.nil?
mapping.each { |pair| self[pair.first] = part.send(pair.last) }
instance_variable_set("@#{name}", part.freeze)
end
end
end
end
end
end
end

View file

@ -1,277 +0,0 @@
module ActiveRecord
module AssociationPreload #:nodoc:
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
# Loads the named associations for the activerecord record (or records) given
# preload_options is passed only one level deep: don't pass to the child associations when associations is a Hash
protected
def preload_associations(records, associations, preload_options={})
records = [records].flatten.compact.uniq
return if records.empty?
case associations
when Array then associations.each {|association| preload_associations(records, association, preload_options)}
when Symbol, String then preload_one_association(records, associations.to_sym, preload_options)
when Hash then
associations.each do |parent, child|
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.first.class.preload_associations(parents, child)
end
end
end
end
private
def preload_one_association(records, association, preload_options={})
class_to_reflection = {}
# Not all records have the same class, so group then preload
# group on the reflection itself so that if various subclass share the same association then we do not split them
# unncessarily
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
send("preload_#{reflection.macro}_association", records, reflection, preload_options)
end
end
def add_preloaded_records_to_collection(parent_records, reflection_name, associated_record)
parent_records.each do |parent_record|
association_proxy = parent_record.send(reflection_name)
association_proxy.loaded
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|
association_proxy = parent_record.send(reflection_name)
association_proxy.loaded
association_proxy.target = associated_record
end
end
def set_association_collection_records(id_to_record_map, reflection_name, associated_records, key)
associated_records.each do |associated_record|
mapped_records = id_to_record_map[associated_record[key].to_s]
add_preloaded_records_to_collection(mapped_records, reflection_name, associated_record)
end
end
def set_association_single_records(id_to_record_map, reflection_name, associated_records, key)
seen_keys = {}
associated_records.each do |associated_record|
#this is a has_one or belongs_to: there should only be one record.
#Unfortunately we can't (in portable way) ask the database for 'all records where foo_id in (x,y,z), but please
# only one row per distinct foo_id' so this where we enforce that
next if seen_keys[associated_record[key].to_s]
seen_keys[associated_record[key].to_s] = true
mapped_records = id_to_record_map[associated_record[key].to_s]
mapped_records.each do |mapped_record|
mapped_record.send("set_#{reflection_name}_target", associated_record)
end
end
end
def construct_id_map(records)
id_to_record_map = {}
ids = []
records.each do |record|
ids << record.id
mapped_records = (id_to_record_map[record.id.to_s] ||= [])
mapped_records << record
end
ids.uniq!
return id_to_record_map, ids
end
def preload_has_and_belongs_to_many_association(records, reflection, preload_options={})
table_name = reflection.klass.quoted_table_name
id_to_record_map, ids = construct_id_map(records)
records.each {|record| record.send(reflection.name).loaded}
options = reflection.options
conditions = "t0.#{reflection.primary_key_name} IN (?)"
conditions << append_conditions(options, 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 _parent_record_id",
:order => options[:order])
set_association_collection_records(id_to_record_map, reflection.name, associated_records, '_parent_record_id')
end
def preload_has_one_association(records, reflection, preload_options={})
id_to_record_map, ids = construct_id_map(records)
options = reflection.options
if options[:through]
records.each {|record| record.send(reflection.name) && record.send(reflection.name).loaded}
through_records = preload_through_records(records, reflection, options[:through])
through_reflection = reflections[options[:through]]
through_primary_key = through_reflection.primary_key_name
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))
end
end
else
records.each {|record| record.send("set_#{reflection.name}_target", nil)}
set_association_single_records(id_to_record_map, reflection.name, find_associated_records(ids, reflection, preload_options), reflection.primary_key_name)
end
end
def preload_has_many_association(records, reflection, preload_options={})
id_to_record_map, ids = construct_id_map(records)
records.each {|record| record.send(reflection.name).loaded}
options = reflection.options
if options[:through]
through_records = preload_through_records(records, reflection, options[:through])
through_reflection = reflections[options[:through]]
through_primary_key = through_reflection.primary_key_name
unless through_records.empty?
source = reflection.source_reflection.name
#add conditions from reflection!
through_records.first.class.preload_associations(through_records, source, reflection.options)
through_records.each do |through_record|
add_preloaded_records_to_collection(id_to_record_map[through_record[through_primary_key].to_s],
reflection.name, through_record.send(source))
end
end
else
set_association_collection_records(id_to_record_map, reflection.name, find_associated_records(ids, reflection, preload_options),
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
if reflection.options[:source_type]
interface = reflection.source_reflection.options[:foreign_type]
preload_options = {:conditions => ["#{connection.quote_column_name interface} = ?", reflection.options[:source_type]]}
records.compact!
records.first.class.preload_associations(records, through_association, preload_options)
# Dont cache the association - we would only be caching a subset
through_records = []
records.each do |record|
proxy = record.send(through_association)
if proxy.respond_to?(:target)
through_records << proxy.target
proxy.reset
else # this is a has_one :through reflection
through_records << proxy if proxy
end
end
through_records.flatten!
else
records.first.class.preload_associations(records, through_association)
through_records = records.map {|record| record.send(through_association)}.flatten
end
through_records.compact!
through_records
end
# FIXME: quoting
def preload_belongs_to_association(records, reflection, preload_options={})
options = reflection.options
primary_key_name = reflection.primary_key_name
if options[:polymorphic]
polymorph_type = options[:foreign_type]
klasses_and_ids = {}
# Construct a mapping from klass to a list of ids to load and a mapping of those ids back to their parent_records
records.each do |record|
if klass = record.send(polymorph_type)
klass_id = record.send(primary_key_name)
if klass_id
id_map = klasses_and_ids[klass] ||= {}
id_list_for_klass_id = (id_map[klass_id.to_s] ||= [])
id_list_for_klass_id << record
end
end
end
klasses_and_ids = klasses_and_ids.to_a
else
id_map = {}
records.each do |record|
key = record.send(primary_key_name)
if key
mapped_records = (id_map[key.to_s] ||= [])
mapped_records << record
end
end
klasses_and_ids = [[reflection.klass.name, id_map]]
end
klasses_and_ids.each do |klass_and_id|
klass_name, id_map = *klass_and_id
klass = klass_name.constantize
table_name = klass.quoted_table_name
primary_key = klass.primary_key
conditions = "#{table_name}.#{primary_key} IN (?)"
conditions << append_conditions(options, preload_options)
associated_records = klass.find(:all, :conditions => [conditions, id_map.keys.uniq],
:include => options[:include],
:select => options[:select],
:joins => options[:joins],
:order => options[:order])
set_association_single_records(id_map, reflection.name, associated_records, primary_key)
end
end
def find_associated_records(ids, reflection, preload_options)
options = reflection.options
table_name = reflection.klass.quoted_table_name
if interface = reflection.options[:as]
conditions = "#{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_id"} IN (?) and #{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_type"} = '#{self.base_class.name.demodulize}'"
else
foreign_key = reflection.primary_key_name
conditions = "#{reflection.klass.quoted_table_name}.#{foreign_key} IN (?)"
end
conditions << append_conditions(options, preload_options)
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
def interpolate_sql_for_preload(sql)
instance_eval("%@#{sql.gsub('@', '\@')}@")
end
def append_conditions(options, preload_options)
sql = ""
sql << " AND (#{interpolate_sql_for_preload(sanitize_sql(options[:conditions]))})" if options[:conditions]
sql << " AND (#{sanitize_sql preload_options[:conditions]})" if preload_options[:conditions]
sql
end
end
end
end

File diff suppressed because it is too large Load diff

View file

@ -1,365 +0,0 @@
require 'set'
module ActiveRecord
module Associations
class AssociationCollection < AssociationProxy #:nodoc:
def initialize(owner, reflection)
super
construct_sql
end
def find(*args)
options = args.extract_options!
# If using a custom finder_sql, scan the entire collection.
if @reflection.options[:finder_sql]
expects_array = args.first.kind_of?(Array)
ids = args.flatten.compact.uniq.map(&:to_i)
if ids.size == 1
id = ids.first
record = load_target.detect { |r| id == r.id }
expects_array ? [ record ] : record
else
load_target.select { |r| ids.include?(r.id) }
end
else
conditions = "#{@finder_sql}"
if sanitized_conditions = sanitize_sql(options[:conditions])
conditions << " AND (#{sanitized_conditions})"
end
options[:conditions] = conditions
if options[:order] && @reflection.options[:order]
options[:order] = "#{options[:order]}, #{@reflection.options[:order]}"
elsif @reflection.options[:order]
options[:order] = @reflection.options[:order]
end
# Build options specific to association
construct_find_options!(options)
merge_options_from_reflection!(options)
# Pass through args exactly as we received them.
args << options
@reflection.klass.find(*args)
end
end
# Fetches the first one using SQL if possible.
def first(*args)
if fetch_first_or_last_using_find? args
find(:first, *args)
else
load_target unless loaded?
@target.first(*args)
end
end
# Fetches the last one using SQL if possible.
def last(*args)
if fetch_first_or_last_using_find? args
find(:last, *args)
else
load_target unless loaded?
@target.last(*args)
end
end
def to_ary
load_target
@target.to_ary
end
def reset
reset_target!
@loaded = false
end
def build(attributes = {})
if attributes.is_a?(Array)
attributes.collect { |attr| build(attr) }
else
build_record(attributes) { |record| set_belongs_to_association_for(record) }
end
end
# Add +records+ to this association. Returns +self+ so method calls may be chained.
# Since << flattens its argument list and inserts each record, +push+ and +concat+ behave identically.
def <<(*records)
result = true
load_target if @owner.new_record?
@owner.transaction do
flatten_deeper(records).each do |record|
raise_on_type_mismatch(record)
add_record_to_target_with_callbacks(record) do |r|
result &&= insert_record(record) unless @owner.new_record?
end
end
end
result && self
end
alias_method :push, :<<
alias_method :concat, :<<
# Remove all records from this association
def delete_all
load_target
delete(@target)
reset_target!
end
# Calculate sum using SQL, not Enumerable
def sum(*args)
if block_given?
calculate(:sum, *args) { |*block_args| yield(*block_args) }
else
calculate(:sum, *args)
end
end
# Remove +records+ from this association. Does not destroy +records+.
def delete(*records)
records = flatten_deeper(records)
records.each { |record| raise_on_type_mismatch(record) }
@owner.transaction do
records.each { |record| callback(:before_remove, record) }
old_records = records.reject {|r| r.new_record? }
delete_records(old_records) if old_records.any?
records.each do |record|
@target.delete(record)
callback(:after_remove, record)
end
end
end
# Removes all records from this association. Returns +self+ so method calls may be chained.
def clear
return self if length.zero? # forces load_target if it hasn't happened already
if @reflection.options[:dependent] && @reflection.options[:dependent] == :destroy
destroy_all
else
delete_all
end
self
end
def destroy_all
@owner.transaction do
each { |record| record.destroy }
end
reset_target!
end
def create(attrs = {})
if attrs.is_a?(Array)
attrs.collect { |attr| create(attr) }
else
create_record(attrs) do |record|
yield(record) if block_given?
record.save
end
end
end
def create!(attrs = {})
create_record(attrs) do |record|
yield(record) if block_given?
record.save!
end
end
# Returns the size of the collection by executing a SELECT COUNT(*) query if the collection hasn't been loaded and
# calling collection.size if it has. If it's more likely than not that the collection does have a size larger than zero
# and you need to fetch that collection afterwards, it'll take one less SELECT query if you use length.
def size
if @owner.new_record? || (loaded? && !@reflection.options[:uniq])
@target.size
elsif !loaded? && !@reflection.options[:uniq] && @target.is_a?(Array)
unsaved_records = Array(@target.detect { |r| r.new_record? })
unsaved_records.size + count_records
else
count_records
end
end
# Returns the size of the collection by loading it and calling size on the array. If you want to use this method to check
# whether the collection is empty, use collection.length.zero? instead of collection.empty?
def length
load_target.size
end
def empty?
size.zero?
end
def any?
if block_given?
method_missing(:any?) { |*block_args| yield(*block_args) }
else
!empty?
end
end
def uniq(collection = self)
seen = Set.new
collection.inject([]) do |kept, record|
unless seen.include?(record.id)
kept << record
seen << record.id
end
kept
end
end
# Replace this collection with +other_array+
# This will perform a diff and delete/add only records that have changed.
def replace(other_array)
other_array.each { |val| raise_on_type_mismatch(val) }
load_target
other = other_array.size < 100 ? other_array : other_array.to_set
current = @target.size < 100 ? @target : @target.to_set
@owner.transaction do
delete(@target.select { |v| !other.include?(v) })
concat(other_array.select { |v| !current.include?(v) })
end
end
def include?(record)
return false unless record.is_a?(@reflection.klass)
load_target if @reflection.options[:finder_sql] && !loaded?
return @target.include?(record) if loaded?
exists?(record)
end
protected
def construct_find_options!(options)
end
def load_target
if !@owner.new_record? || foreign_key_present
begin
if !loaded?
if @target.is_a?(Array) && @target.any?
@target = find_target + @target.find_all {|t| t.new_record? }
else
@target = find_target
end
end
rescue ActiveRecord::RecordNotFound
reset
end
end
loaded if target
target
end
def method_missing(method, *args)
if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method))
if block_given?
super { |*block_args| yield(*block_args) }
else
super
end
elsif @reflection.klass.scopes.include?(method)
@reflection.klass.scopes[method].call(self, *args)
else
with_scope(construct_scope) do
if block_given?
@reflection.klass.send(method, *args) { |*block_args| yield(*block_args) }
else
@reflection.klass.send(method, *args)
end
end
end
end
# overloaded in derived Association classes to provide useful scoping depending on association type.
def construct_scope
{}
end
def reset_target!
@target = Array.new
end
def find_target
records =
if @reflection.options[:finder_sql]
@reflection.klass.find_by_sql(@finder_sql)
else
find(:all)
end
@reflection.options[:uniq] ? uniq(records) : records
end
private
def create_record(attrs)
attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash)
ensure_owner_is_not_new
record = @reflection.klass.send(:with_scope, :create => construct_scope[:create]) { @reflection.klass.new(attrs) }
if block_given?
add_record_to_target_with_callbacks(record) { |*block_args| yield(*block_args) }
else
add_record_to_target_with_callbacks(record)
end
end
def build_record(attrs)
attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash)
record = @reflection.klass.new(attrs)
if block_given?
add_record_to_target_with_callbacks(record) { |*block_args| yield(*block_args) }
else
add_record_to_target_with_callbacks(record)
end
end
def add_record_to_target_with_callbacks(record)
callback(:before_add, record)
yield(record) if block_given?
@target ||= [] unless loaded?
@target << record
callback(:after_add, record)
record
end
def callback(method, record)
callbacks_for(method).each do |callback|
ActiveSupport::Callbacks::Callback.new(method, callback, record).call(@owner, record)
end
end
def callbacks_for(callback_name)
full_callback_name = "#{callback_name}_for_#{@reflection.name}"
@owner.class.read_inheritable_attribute(full_callback_name.to_sym) || []
end
def ensure_owner_is_not_new
if @owner.new_record?
raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved"
end
end
def fetch_first_or_last_using_find?(args)
args.first.kind_of?(Hash) || !(loaded? || @owner.new_record? || @reflection.options[:finder_sql] || !@target.blank? || args.first.kind_of?(Integer))
end
end
end
end

View file

@ -1,224 +0,0 @@
module ActiveRecord
module Associations
# This is the root class of all association proxies:
#
# AssociationProxy
# BelongsToAssociation
# HasOneAssociation
# BelongsToPolymorphicAssociation
# AssociationCollection
# HasAndBelongsToManyAssociation
# HasManyAssociation
# HasManyThroughAssociation
# HasOneThroughAssociation
#
# Association proxies in Active Record are middlemen between the object that
# holds the association, known as the <tt>@owner</tt>, and the actual associated
# object, known as the <tt>@target</tt>. The kind of association any proxy is
# about is available in <tt>@reflection</tt>. That's an instance of the class
# ActiveRecord::Reflection::AssociationReflection.
#
# For example, given
#
# class Blog < ActiveRecord::Base
# has_many :posts
# end
#
# blog = Blog.find(:first)
#
# the association proxy in <tt>blog.posts</tt> has the object in +blog+ as
# <tt>@owner</tt>, the collection of its posts as <tt>@target</tt>, and
# the <tt>@reflection</tt> object represents a <tt>:has_many</tt> macro.
#
# This class has most of the basic instance methods removed, and delegates
# unknown methods to <tt>@target</tt> via <tt>method_missing</tt>. As a
# corner case, it even removes the +class+ method and that's why you get
#
# blog.posts.class # => Array
#
# though the object behind <tt>blog.posts</tt> is not an Array, but an
# ActiveRecord::Associations::HasManyAssociation.
#
# The <tt>@target</tt> object is not loaded until needed. For example,
#
# blog.posts.count
#
# is computed directly through SQL and does not trigger by itself the
# instantiation of the actual post records.
class AssociationProxy #:nodoc:
alias_method :proxy_respond_to?, :respond_to?
alias_method :proxy_extend, :extend
delegate :to_param, :to => :proxy_target
instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?$|^send$|proxy_|^object_id$)/ }
def initialize(owner, reflection)
@owner, @reflection = owner, reflection
Array(reflection.options[:extend]).each { |ext| proxy_extend(ext) }
reset
end
def proxy_owner
@owner
end
def proxy_reflection
@reflection
end
def proxy_target
@target
end
def respond_to?(symbol, include_priv = false)
proxy_respond_to?(symbol, include_priv) || (load_target && @target.respond_to?(symbol, include_priv))
end
# Explicitly proxy === because the instance method removal above
# doesn't catch it.
def ===(other)
load_target
other === @target
end
def aliased_table_name
@reflection.klass.table_name
end
def conditions
@conditions ||= interpolate_sql(sanitize_sql(@reflection.options[:conditions])) if @reflection.options[:conditions]
end
alias :sql_conditions :conditions
def reset
@loaded = false
@target = nil
end
def reload
reset
load_target
self unless @target.nil?
end
def loaded?
@loaded
end
def loaded
@loaded = true
end
def target
@target
end
def target=(target)
@target = target
loaded
end
def inspect
load_target
@target.inspect
end
protected
def dependent?
@reflection.options[:dependent]
end
def quoted_record_ids(records)
records.map { |record| record.quoted_id }.join(',')
end
def interpolate_sql_options!(options, *keys)
keys.each { |key| options[key] &&= interpolate_sql(options[key]) }
end
def interpolate_sql(sql, record = nil)
@owner.send(:interpolate_sql, sql, record)
end
def sanitize_sql(sql)
@reflection.klass.send(:sanitize_sql, sql)
end
def set_belongs_to_association_for(record)
if @reflection.options[:as]
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?
end
end
def merge_options_from_reflection!(options)
options.reverse_merge!(
:group => @reflection.options[:group],
:limit => @reflection.options[:limit],
:offset => @reflection.options[:offset],
:joins => @reflection.options[:joins],
:include => @reflection.options[:include],
:select => @reflection.options[:select],
:readonly => @reflection.options[:readonly]
)
end
def with_scope(*args, &block)
@reflection.klass.send :with_scope, *args, &block
end
private
def method_missing(method, *args)
if load_target
if block_given?
@target.send(method, *args) { |*block_args| yield(*block_args) }
else
@target.send(method, *args)
end
end
end
# Loads the target if needed and returns it.
#
# This method is abstract in the sense that it relies on +find_target+,
# which is expected to be provided by descendants.
#
# If the target is already loaded it is just returned. Thus, you can call
# +load_target+ unconditionally to get the target.
#
# ActiveRecord::RecordNotFound is rescued within the method, and it is
# not reraised. The proxy is reset and +nil+ is the return value.
def load_target
return nil unless defined?(@loaded)
if !loaded? and (!@owner.new_record? || foreign_key_present)
@target = find_target
end
@loaded = true
@target
rescue ActiveRecord::RecordNotFound
reset
end
# Can be overwritten by associations that might have the foreign key available for an association without
# having the object itself (and still being a new record). Currently, only belongs_to presents this scenario.
def foreign_key_present
false
end
def raise_on_type_mismatch(record)
unless record.is_a?(@reflection.klass)
message = "#{@reflection.class_name}(##{@reflection.klass.object_id}) expected, got #{record.class}(##{record.class.object_id})"
raise ActiveRecord::AssociationTypeMismatch, message
end
end
# Array#flatten has problems with recursive arrays. Going one level deeper solves the majority of the problems.
def flatten_deeper(array)
array.collect { |element| element.respond_to?(:flatten) ? element.flatten : element }.flatten
end
end
end
end

View file

@ -1,58 +0,0 @@
module ActiveRecord
module Associations
class BelongsToAssociation < AssociationProxy #:nodoc:
def create(attributes = {})
replace(@reflection.klass.create(attributes))
end
def build(attributes = {})
replace(@reflection.klass.new(attributes))
end
def replace(record)
counter_cache_name = @reflection.counter_cache_column
if record.nil?
if counter_cache_name && !@owner.new_record?
@reflection.klass.decrement_counter(counter_cache_name, @owner[@reflection.primary_key_name]) if @owner[@reflection.primary_key_name]
end
@target = @owner[@reflection.primary_key_name] = nil
else
raise_on_type_mismatch(record)
if counter_cache_name && !@owner.new_record?
@reflection.klass.increment_counter(counter_cache_name, record.id)
@reflection.klass.decrement_counter(counter_cache_name, @owner[@reflection.primary_key_name]) if @owner[@reflection.primary_key_name]
end
@target = (AssociationProxy === record ? record.target : record)
@owner[@reflection.primary_key_name] = record.id unless record.new_record?
@updated = true
end
loaded
record
end
def updated?
@updated
end
private
def find_target
@reflection.klass.find(
@owner[@reflection.primary_key_name],
:select => @reflection.options[:select],
:conditions => conditions,
:include => @reflection.options[:include],
:readonly => @reflection.options[:readonly]
)
end
def foreign_key_present
!@owner[@reflection.primary_key_name].nil?
end
end
end
end

View file

@ -1,49 +0,0 @@
module ActiveRecord
module Associations
class BelongsToPolymorphicAssociation < AssociationProxy #:nodoc:
def replace(record)
if record.nil?
@target = @owner[@reflection.primary_key_name] = @owner[@reflection.options[:foreign_type]] = nil
else
@target = (AssociationProxy === record ? record.target : record)
@owner[@reflection.primary_key_name] = record.id
@owner[@reflection.options[:foreign_type]] = record.class.base_class.name.to_s
@updated = true
end
loaded
record
end
def updated?
@updated
end
private
def find_target
return nil if association_class.nil?
if @reflection.options[:conditions]
association_class.find(
@owner[@reflection.primary_key_name],
:select => @reflection.options[:select],
:conditions => conditions,
:include => @reflection.options[:include]
)
else
association_class.find(@owner[@reflection.primary_key_name], :select => @reflection.options[:select], :include => @reflection.options[:include])
end
end
def foreign_key_present
!@owner[@reflection.primary_key_name].nil?
end
def association_class
@owner[@reflection.options[:foreign_type]] ? @owner[@reflection.options[:foreign_type]].constantize : nil
end
end
end
end

View file

@ -1,112 +0,0 @@
module ActiveRecord
module Associations
class HasAndBelongsToManyAssociation < AssociationCollection #:nodoc:
def create(attributes = {})
create_record(attributes) { |record| insert_record(record) }
end
def create!(attributes = {})
create_record(attributes) { |record| insert_record(record, true) }
end
protected
def construct_find_options!(options)
options[:joins] = @join_sql
options[:readonly] = finding_with_ambiguous_select?(options[:select] || @reflection.options[:select])
options[:select] ||= (@reflection.options[:select] || '*')
end
def count_records
load_target.size
end
def insert_record(record, force=true)
if record.new_record?
if force
record.save!
else
return false unless record.save
end
end
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
attrs[column.name] = @owner.quoted_id
when @reflection.association_foreign_key.to_s
attrs[column.name] = record.quoted_id
else
if record.has_attribute?(column.name)
value = @owner.send(:quote_value, record[column.name], column)
attrs[column.name] = value unless value.nil?
end
end
attrs
end
sql =
"INSERT INTO #{@owner.connection.quote_table_name @reflection.options[:join_table]} (#{@owner.send(:quoted_column_names, attributes).join(', ')}) " +
"VALUES (#{attributes.values.join(', ')})"
@owner.connection.insert(sql)
end
return true
end
def delete_records(records)
if sql = @reflection.options[:delete_sql]
records.each { |record| @owner.connection.delete(interpolate_sql(sql, record)) }
else
ids = quoted_record_ids(records)
sql = "DELETE FROM #{@owner.connection.quote_table_name @reflection.options[:join_table]} WHERE #{@reflection.primary_key_name} = #{@owner.quoted_id} AND #{@reflection.association_foreign_key} IN (#{ids})"
@owner.connection.delete(sql)
end
end
def construct_sql
interpolate_sql_options!(@reflection.options, :finder_sql)
if @reflection.options[:finder_sql]
@finder_sql = @reflection.options[:finder_sql]
else
@finder_sql = "#{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.primary_key_name} = #{@owner.quoted_id} "
@finder_sql << " AND (#{conditions})" if conditions
end
@join_sql = "INNER JOIN #{@owner.connection.quote_table_name @reflection.options[:join_table]} ON #{@reflection.quoted_table_name}.#{@reflection.klass.primary_key} = #{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.association_foreign_key}"
end
def construct_scope
{ :find => { :conditions => @finder_sql,
:joins => @join_sql,
:readonly => false,
:order => @reflection.options[:order],
:limit => @reflection.options[:limit] } }
end
# Join tables with additional columns on top of the two foreign keys must be considered ambiguous unless a select
# 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
end
private
def create_record(attributes, &block)
# Can't use Base.create because the foreign key may be a protected attribute.
ensure_owner_is_not_new
if attributes.is_a?(Array)
attributes.collect { |attr| create(attr) }
else
build_record(attributes, &block)
end
end
end
end
end

View file

@ -1,109 +0,0 @@
module ActiveRecord
module Associations
class HasManyAssociation < AssociationCollection #:nodoc:
# Count the number of associated records. All arguments are optional.
def count(*args)
if @reflection.options[:counter_sql]
@reflection.klass.count_by_sql(@counter_sql)
elsif @reflection.options[:finder_sql]
@reflection.klass.count_by_sql(@finder_sql)
else
column_name, options = @reflection.klass.send(:construct_count_options_from_args, *args)
options[:conditions] = options[:conditions].blank? ?
@finder_sql :
@finder_sql + " AND (#{sanitize_sql(options[:conditions])})"
options[:include] ||= @reflection.options[:include]
@reflection.klass.count(column_name, options)
end
end
protected
def count_records
count = if has_cached_counter?
@owner.send(:read_attribute, cached_counter_attribute_name)
elsif @reflection.options[:counter_sql]
@reflection.klass.count_by_sql(@counter_sql)
else
@reflection.klass.count(:conditions => @counter_sql, :include => @reflection.options[:include])
end
@target = [] and loaded if count == 0
if @reflection.options[:limit]
count = [ @reflection.options[:limit], count ].min
end
return count
end
def has_cached_counter?
@owner.attribute_present?(cached_counter_attribute_name)
end
def cached_counter_attribute_name
"#{@reflection.name}_count"
end
def insert_record(record)
set_belongs_to_association_for(record)
record.save
end
def delete_records(records)
case @reflection.options[:dependent]
when :destroy
records.each(&:destroy)
when :delete_all
@reflection.klass.delete(records.map(&:id))
else
ids = quoted_record_ids(records)
@reflection.klass.update_all(
"#{@reflection.primary_key_name} = NULL",
"#{@reflection.primary_key_name} = #{@owner.quoted_id} AND #{@reflection.klass.primary_key} IN (#{ids})"
)
end
end
def target_obsolete?
false
end
def construct_sql
case
when @reflection.options[:finder_sql]
@finder_sql = interpolate_sql(@reflection.options[:finder_sql])
when @reflection.options[:as]
@finder_sql =
"#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_id = #{@owner.quoted_id} AND " +
"#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}"
@finder_sql << " AND (#{conditions})" if conditions
else
@finder_sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{@owner.quoted_id}"
@finder_sql << " AND (#{conditions})" if conditions
end
if @reflection.options[:counter_sql]
@counter_sql = interpolate_sql(@reflection.options[:counter_sql])
elsif @reflection.options[:finder_sql]
# replace the SELECT clause with COUNT(*), preserving any hints within /* ... */
@reflection.options[:counter_sql] = @reflection.options[:finder_sql].sub(/SELECT (\/\*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" }
@counter_sql = interpolate_sql(@reflection.options[:counter_sql])
else
@counter_sql = @finder_sql
end
end
def construct_scope
create_scoping = {}
set_belongs_to_association_for(create_scoping)
{
:find => { :conditions => @finder_sql, :readonly => false, :order => @reflection.options[:order], :limit => @reflection.options[:limit] },
:create => create_scoping
}
end
end
end
end

View file

@ -1,254 +0,0 @@
module ActiveRecord
module Associations
class HasManyThroughAssociation < HasManyAssociation #:nodoc:
def initialize(owner, reflection)
reflection.check_validity!
super
end
alias_method :new, :build
def create!(attrs = nil)
@reflection.klass.transaction do
self << (object = attrs ? @reflection.klass.send(:with_scope, :create => attrs) { @reflection.klass.create! } : @reflection.klass.create!)
object
end
end
def create(attrs = nil)
@reflection.klass.transaction do
self << (object = attrs ? @reflection.klass.send(:with_scope, :create => attrs) { @reflection.klass.create } : @reflection.klass.create)
object
end
end
# Returns the size of the collection by executing a SELECT COUNT(*) query if the collection hasn't been loaded and
# calling collection.size if it has. If it's more likely than not that the collection does have a size larger than zero
# and you need to fetch that collection afterwards, it'll take one less SELECT query if you use length.
def size
return @owner.send(:read_attribute, cached_counter_attribute_name) if has_cached_counter?
return @target.size if loaded?
return count
end
def count(*args)
column_name, options = @reflection.klass.send(:construct_count_options_from_args, *args)
if @reflection.options[:uniq]
# This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL statement.
column_name = "#{@reflection.quoted_table_name}.#{@reflection.klass.primary_key}" if column_name == :all
options.merge!(:distinct => true)
end
@reflection.klass.send(:with_scope, construct_scope) { @reflection.klass.count(column_name, options) }
end
protected
def construct_find_options!(options)
options[:select] = construct_select(options[:select])
options[:from] ||= construct_from
options[:joins] = construct_joins(options[:joins])
options[:include] = @reflection.source_reflection.options[:include] if options[:include].nil?
end
def insert_record(record, force=true)
if record.new_record?
if force
record.save!
else
return false unless record.save
end
end
klass = @reflection.through_reflection.klass
@owner.send(@reflection.through_reflection.name).proxy_target << klass.send(:with_scope, :create => construct_join_attributes(record)) { klass.create! }
end
# TODO - add dependent option support
def delete_records(records)
klass = @reflection.through_reflection.klass
records.each do |associate|
klass.delete_all(construct_join_attributes(associate))
end
end
def find_target
@reflection.klass.find(:all,
:select => construct_select,
:conditions => construct_conditions,
:from => construct_from,
:joins => construct_joins,
:order => @reflection.options[:order],
:limit => @reflection.options[:limit],
:group => @reflection.options[:group],
:readonly => @reflection.options[:readonly],
:include => @reflection.options[:include] || @reflection.source_reflection.options[:include]
)
end
# Construct attributes for associate pointing to owner.
def construct_owner_attributes(reflection)
if as = reflection.options[:as]
{ "#{as}_id" => @owner.id,
"#{as}_type" => @owner.class.base_class.name.to_s }
else
{ reflection.primary_key_name => @owner.id }
end
end
# Construct attributes for :through pointing to owner and associate.
def construct_join_attributes(associate)
# TODO: revist this to allow it for deletion, supposing dependent option is supported
raise ActiveRecord::HasManyThroughCantAssociateThroughHasManyReflection.new(@owner, @reflection) if @reflection.source_reflection.macro == :has_many
join_attributes = construct_owner_attributes(@reflection.through_reflection).merge(@reflection.source_reflection.primary_key_name => associate.id)
if @reflection.options[:source_type]
join_attributes.merge!(@reflection.source_reflection.options[:foreign_type] => associate.class.base_class.name.to_s)
end
join_attributes
end
# Associate attributes pointing to owner, quoted.
def construct_quoted_owner_attributes(reflection)
if as = reflection.options[:as]
{ "#{as}_id" => @owner.quoted_id,
"#{as}_type" => reflection.klass.quote_value(
@owner.class.base_class.name.to_s,
reflection.klass.columns_hash["#{as}_type"]) }
else
{ reflection.primary_key_name => @owner.quoted_id }
end
end
# Build SQL conditions from attributes, qualified by table name.
def construct_conditions
table_name = @reflection.through_reflection.quoted_table_name
conditions = construct_quoted_owner_attributes(@reflection.through_reflection).map do |attr, value|
"#{table_name}.#{attr} = #{value}"
end
conditions << sql_conditions if sql_conditions
"(" + conditions.join(') AND (') + ")"
end
def construct_from
@reflection.quoted_table_name
end
def construct_select(custom_select = nil)
distinct = "DISTINCT " if @reflection.options[:uniq]
selected = custom_select || @reflection.options[:select] || "#{distinct}#{@reflection.quoted_table_name}.*"
end
def construct_joins(custom_joins = nil)
polymorphic_join = nil
if @reflection.source_reflection.macro == :belongs_to
reflection_primary_key = @reflection.klass.primary_key
source_primary_key = @reflection.source_reflection.primary_key_name
if @reflection.options[:source_type]
polymorphic_join = "AND %s.%s = %s" % [
@reflection.through_reflection.quoted_table_name, "#{@reflection.source_reflection.options[:foreign_type]}",
@owner.class.quote_value(@reflection.options[:source_type])
]
end
else
reflection_primary_key = @reflection.source_reflection.primary_key_name
source_primary_key = @reflection.klass.primary_key
if @reflection.source_reflection.options[:as]
polymorphic_join = "AND %s.%s = %s" % [
@reflection.quoted_table_name, "#{@reflection.source_reflection.options[:as]}_type",
@owner.class.quote_value(@reflection.through_reflection.klass.name)
]
end
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,
polymorphic_join
]
end
def construct_scope
{ :create => construct_owner_attributes(@reflection),
:find => { :from => construct_from,
:conditions => construct_conditions,
:joins => construct_joins,
:include => @reflection.options[:include],
:select => construct_select,
:order => @reflection.options[:order],
:limit => @reflection.options[:limit],
:readonly => @reflection.options[:readonly],
} }
end
def construct_sql
case
when @reflection.options[:finder_sql]
@finder_sql = interpolate_sql(@reflection.options[:finder_sql])
@finder_sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{@owner.quoted_id}"
@finder_sql << " AND (#{conditions})" if conditions
else
@finder_sql = construct_conditions
end
if @reflection.options[:counter_sql]
@counter_sql = interpolate_sql(@reflection.options[:counter_sql])
elsif @reflection.options[:finder_sql]
# replace the SELECT clause with COUNT(*), preserving any hints within /* ... */
@reflection.options[:counter_sql] = @reflection.options[:finder_sql].sub(/SELECT (\/\*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" }
@counter_sql = interpolate_sql(@reflection.options[:counter_sql])
else
@counter_sql = @finder_sql
end
end
def conditions
@conditions = build_conditions unless defined?(@conditions)
@conditions
end
def build_conditions
association_conditions = @reflection.options[:conditions]
through_conditions = build_through_conditions
source_conditions = @reflection.source_reflection.options[:conditions]
uses_sti = !@reflection.through_reflection.klass.descends_from_active_record?
if association_conditions || through_conditions || source_conditions || uses_sti
all = []
[association_conditions, source_conditions].each do |conditions|
all << interpolate_sql(sanitize_sql(conditions)) if conditions
end
all << through_conditions if through_conditions
all << build_sti_condition if uses_sti
all.map { |sql| "(#{sql})" } * ' AND '
end
end
def build_through_conditions
conditions = @reflection.through_reflection.options[:conditions]
if conditions.is_a?(Hash)
interpolate_sql(sanitize_sql(conditions)).gsub(
@reflection.quoted_table_name,
@reflection.through_reflection.quoted_table_name)
elsif conditions
interpolate_sql(sanitize_sql(conditions))
end
end
def build_sti_condition
"#{@reflection.through_reflection.quoted_table_name}.#{@reflection.through_reflection.klass.inheritance_column} = #{@reflection.klass.quote_value(@reflection.through_reflection.klass.sti_name)}"
end
alias_method :sql_conditions, :conditions
def has_cached_counter?
@owner.attribute_present?(cached_counter_attribute_name)
end
def cached_counter_attribute_name
"#{@reflection.name}_count"
end
end
end
end

View file

@ -1,98 +0,0 @@
module ActiveRecord
module Associations
class HasOneAssociation < BelongsToAssociation #:nodoc:
def initialize(owner, reflection)
super
construct_sql
end
def create(attrs = {}, replace_existing = true)
new_record(replace_existing) { |klass| klass.create(attrs) }
end
def create!(attrs = {}, replace_existing = true)
new_record(replace_existing) { |klass| klass.create!(attrs) }
end
def build(attrs = {}, replace_existing = true)
new_record(replace_existing) { |klass| klass.new(attrs) }
end
def replace(obj, dont_save = false)
load_target
unless @target.nil?
if dependent? && !dont_save && @target != obj
@target.destroy unless @target.new_record?
@owner.clear_association_cache
else
@target[@reflection.primary_key_name] = nil
@target.save unless @owner.new_record? || @target.new_record?
end
end
if obj.nil?
@target = nil
else
raise_on_type_mismatch(obj)
set_belongs_to_association_for(obj)
@target = (AssociationProxy === obj ? obj.target : obj)
end
@loaded = true
unless @owner.new_record? or obj.nil? or dont_save
return (obj.save ? self : false)
else
return (obj.nil? ? nil : self)
end
end
private
def find_target
@reflection.klass.find(:first,
:conditions => @finder_sql,
:select => @reflection.options[:select],
:order => @reflection.options[:order],
:include => @reflection.options[:include],
:readonly => @reflection.options[:readonly]
)
end
def construct_sql
case
when @reflection.options[:as]
@finder_sql =
"#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_id = #{@owner.quoted_id} AND " +
"#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}"
else
@finder_sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{@owner.quoted_id}"
end
@finder_sql << " AND (#{conditions})" if conditions
end
def construct_scope
create_scoping = {}
set_belongs_to_association_for(create_scoping)
{ :create => create_scoping }
end
def new_record(replace_existing)
# Make sure we load the target first, if we plan on replacing the existing
# instance. Otherwise, if the target has not previously been loaded
# elsewhere, the instance we create will get orphaned.
load_target if replace_existing
record = @reflection.klass.send(:with_scope, :create => construct_scope[:create]) { yield @reflection.klass }
if replace_existing
replace(record, true)
else
record[@reflection.primary_key_name] = @owner.id unless @owner.new_record?
self.target = record
end
record
end
end
end
end

View file

@ -1,28 +0,0 @@
module ActiveRecord
module Associations
class HasOneThroughAssociation < HasManyThroughAssociation
def create_through_record(new_value) #nodoc:
klass = @reflection.through_reflection.klass
current_object = @owner.send(@reflection.through_reflection.name)
if current_object
klass.destroy(current_object)
@owner.clear_association_cache
end
@owner.send(@reflection.through_reflection.name, klass.send(:create, construct_join_attributes(new_value)))
end
private
def find(*args)
super(args.merge(:limit => 1))
end
def find_target
super.first
end
end
end
end

View file

@ -1,379 +0,0 @@
module ActiveRecord
module AttributeMethods #:nodoc:
DEFAULT_SUFFIXES = %w(= ? _before_type_cast)
ATTRIBUTE_TYPES_CACHED_BY_DEFAULT = [:datetime, :timestamp, :time, :date]
def self.included(base)
base.extend ClassMethods
base.attribute_method_suffix(*DEFAULT_SUFFIXES)
base.cattr_accessor :attribute_types_cached_by_default, :instance_writer => false
base.attribute_types_cached_by_default = ATTRIBUTE_TYPES_CACHED_BY_DEFAULT
base.cattr_accessor :time_zone_aware_attributes, :instance_writer => false
base.time_zone_aware_attributes = false
base.cattr_accessor :skip_time_zone_conversion_for_attributes, :instance_writer => false
base.skip_time_zone_conversion_for_attributes = []
end
# Declare and check for suffixed attribute methods.
module ClassMethods
# Declares a method available for all attributes with the given suffix.
# Uses +method_missing+ and <tt>respond_to?</tt> to rewrite the method
#
# #{attr}#{suffix}(*args, &block)
#
# to
#
# attribute#{suffix}(#{attr}, *args, &block)
#
# An <tt>attribute#{suffix}</tt> instance method must exist and accept at least
# the +attr+ argument.
#
# For example:
#
# class Person < ActiveRecord::Base
# attribute_method_suffix '_changed?'
#
# private
# def attribute_changed?(attr)
# ...
# end
# end
#
# person = Person.find(1)
# person.name_changed? # => false
# person.name = 'Hubert'
# person.name_changed? # => true
def attribute_method_suffix(*suffixes)
attribute_method_suffixes.concat suffixes
rebuild_attribute_method_regexp
end
# Returns MatchData if method_name is an attribute method.
def match_attribute_method?(method_name)
rebuild_attribute_method_regexp unless defined?(@@attribute_method_regexp) && @@attribute_method_regexp
@@attribute_method_regexp.match(method_name)
end
# Contains the names of the generated attribute methods.
def generated_methods #:nodoc:
@generated_methods ||= Set.new
end
def generated_methods?
!generated_methods.empty?
end
# Generates all the attribute related methods for columns in the database
# accessors, mutators and query methods.
def define_attribute_methods
return if generated_methods?
columns_hash.each do |name, column|
unless instance_method_already_implemented?(name)
if self.serialized_attributes[name]
define_read_method_for_serialized_attribute(name)
elsif create_time_zone_conversion_attribute?(name, column)
define_read_method_for_time_zone_conversion(name)
else
define_read_method(name.to_sym, name, column)
end
end
unless instance_method_already_implemented?("#{name}=")
if create_time_zone_conversion_attribute?(name, column)
define_write_method_for_time_zone_conversion(name)
else
define_write_method(name.to_sym)
end
end
unless instance_method_already_implemented?("#{name}?")
define_question_method(name)
end
end
end
# Checks whether the method is defined in the model or any of its subclasses
# that also derive from Active Record. Raises DangerousAttributeError if the
# method is defined by Active Record though.
def instance_method_already_implemented?(method_name)
method_name = method_name.to_s
return true if method_name =~ /^id(=$|\?$|$)/
@_defined_class_methods ||= ancestors.first(ancestors.index(ActiveRecord::Base)).sum([]) { |m| m.public_instance_methods(false) | m.private_instance_methods(false) | m.protected_instance_methods(false) }.map(&:to_s).to_set
@@_defined_activerecord_methods ||= (ActiveRecord::Base.public_instance_methods(false) | ActiveRecord::Base.private_instance_methods(false) | ActiveRecord::Base.protected_instance_methods(false)).map(&:to_s).to_set
raise DangerousAttributeError, "#{method_name} is defined by ActiveRecord" if @@_defined_activerecord_methods.include?(method_name)
@_defined_class_methods.include?(method_name)
end
alias :define_read_methods :define_attribute_methods
# +cache_attributes+ allows you to declare which converted attribute values should
# be cached. Usually caching only pays off for attributes with expensive conversion
# methods, like time related columns (e.g. +created_at+, +updated_at+).
def cache_attributes(*attribute_names)
attribute_names.each {|attr| cached_attributes << attr.to_s}
end
# Returns the attributes which are cached. By default time related columns
# with datatype <tt>:datetime, :timestamp, :time, :date</tt> are cached.
def cached_attributes
@cached_attributes ||=
columns.select{|c| attribute_types_cached_by_default.include?(c.type)}.map(&:name).to_set
end
# Returns +true+ if the provided attribute is being cached.
def cache_attribute?(attr_name)
cached_attributes.include?(attr_name)
end
private
# Suffixes a, ?, c become regexp /(a|\?|c)$/
def rebuild_attribute_method_regexp
suffixes = attribute_method_suffixes.map { |s| Regexp.escape(s) }
@@attribute_method_regexp = /(#{suffixes.join('|')})$/.freeze
end
# Default to =, ?, _before_type_cast
def attribute_method_suffixes
@@attribute_method_suffixes ||= []
end
def create_time_zone_conversion_attribute?(name, column)
time_zone_aware_attributes && !skip_time_zone_conversion_for_attributes.include?(name.to_sym) && [:datetime, :timestamp].include?(column.type)
end
# Define an attribute reader method. Cope with nil column.
def define_read_method(symbol, attr_name, column)
cast_code = column.type_cast_code('v') if column
access_code = cast_code ? "(v=@attributes['#{attr_name}']) && #{cast_code}" : "@attributes['#{attr_name}']"
unless attr_name.to_s == self.primary_key.to_s
access_code = access_code.insert(0, "missing_attribute('#{attr_name}', caller) unless @attributes.has_key?('#{attr_name}'); ")
end
if cache_attribute?(attr_name)
access_code = "@attributes_cache['#{attr_name}'] ||= (#{access_code})"
end
evaluate_attribute_method attr_name, "def #{symbol}; #{access_code}; end"
end
# Define read method for serialized attribute.
def define_read_method_for_serialized_attribute(attr_name)
evaluate_attribute_method attr_name, "def #{attr_name}; unserialize_attribute('#{attr_name}'); end"
end
# Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled.
# This enhanced read method automatically converts the UTC time stored in the database to the time zone stored in Time.zone.
def define_read_method_for_time_zone_conversion(attr_name)
method_body = <<-EOV
def #{attr_name}(reload = false)
cached = @attributes_cache['#{attr_name}']
return cached if cached && !reload
time = read_attribute('#{attr_name}')
@attributes_cache['#{attr_name}'] = time.acts_like?(:time) ? time.in_time_zone : time
end
EOV
evaluate_attribute_method attr_name, method_body
end
# Defines a predicate method <tt>attr_name?</tt>.
def define_question_method(attr_name)
evaluate_attribute_method attr_name, "def #{attr_name}?; query_attribute('#{attr_name}'); end", "#{attr_name}?"
end
def define_write_method(attr_name)
evaluate_attribute_method attr_name, "def #{attr_name}=(new_value);write_attribute('#{attr_name}', new_value);end", "#{attr_name}="
end
# Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled.
# This enhanced write method will automatically convert the time passed to it to the zone stored in Time.zone.
def define_write_method_for_time_zone_conversion(attr_name)
method_body = <<-EOV
def #{attr_name}=(time)
unless time.acts_like?(:time)
time = time.is_a?(String) ? Time.zone.parse(time) : time.to_time rescue time
end
time = time.in_time_zone rescue nil if time
write_attribute(:#{attr_name}, time)
end
EOV
evaluate_attribute_method attr_name, method_body, "#{attr_name}="
end
# Evaluate the definition for an attribute related method
def evaluate_attribute_method(attr_name, method_definition, method_name=attr_name)
unless method_name.to_s == primary_key.to_s
generated_methods << method_name
end
begin
class_eval(method_definition, __FILE__, __LINE__)
rescue SyntaxError => err
generated_methods.delete(attr_name)
if logger
logger.warn "Exception occurred during reader method compilation."
logger.warn "Maybe #{attr_name} is not a valid Ruby identifier?"
logger.warn "#{err.message}"
end
end
end
end # ClassMethods
# Allows access to the object attributes, which are held in the <tt>@attributes</tt> hash, as though they
# were first-class methods. So a Person class with a name attribute can use Person#name and
# Person#name= and never directly use the attributes hash -- except for multiple assigns with
# ActiveRecord#attributes=. A Milestone class can also ask Milestone#completed? to test that
# the completed attribute is not +nil+ or 0.
#
# It's also possible to instantiate related objects, so a Client class belonging to the clients
# table with a +master_id+ foreign key can instantiate master through Client#master.
def method_missing(method_id, *args, &block)
method_name = method_id.to_s
# If we haven't generated any methods yet, generate them, then
# see if we've created the method we're looking for.
if !self.class.generated_methods?
self.class.define_attribute_methods
if self.class.generated_methods.include?(method_name)
return self.send(method_id, *args, &block)
end
end
if self.class.primary_key.to_s == method_name
id
elsif md = self.class.match_attribute_method?(method_name)
attribute_name, method_type = md.pre_match, md.to_s
if @attributes.include?(attribute_name)
__send__("attribute#{method_type}", attribute_name, *args, &block)
else
super
end
elsif @attributes.include?(method_name)
read_attribute(method_name)
else
super
end
end
# Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example,
# "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)).
def read_attribute(attr_name)
attr_name = attr_name.to_s
if !(value = @attributes[attr_name]).nil?
if column = column_for_attribute(attr_name)
if unserializable_attribute?(attr_name, column)
unserialize_attribute(attr_name)
else
column.type_cast(value)
end
else
value
end
else
nil
end
end
def read_attribute_before_type_cast(attr_name)
@attributes[attr_name]
end
# Returns true if the attribute is of a text column and marked for serialization.
def unserializable_attribute?(attr_name, column)
column.text? && self.class.serialized_attributes[attr_name]
end
# Returns the unserialized object of the attribute.
def unserialize_attribute(attr_name)
unserialized_object = object_from_yaml(@attributes[attr_name])
if unserialized_object.is_a?(self.class.serialized_attributes[attr_name]) || unserialized_object.nil?
@attributes.frozen? ? unserialized_object : @attributes[attr_name] = unserialized_object
else
raise SerializationTypeMismatch,
"#{attr_name} was supposed to be a #{self.class.serialized_attributes[attr_name]}, but was a #{unserialized_object.class.to_s}"
end
end
# Updates the attribute identified by <tt>attr_name</tt> with the specified +value+. Empty strings for fixnum and float
# columns are turned into +nil+.
def write_attribute(attr_name, value)
attr_name = attr_name.to_s
@attributes_cache.delete(attr_name)
if (column = column_for_attribute(attr_name)) && column.number?
@attributes[attr_name] = convert_number_column_value(value)
else
@attributes[attr_name] = value
end
end
def query_attribute(attr_name)
unless value = read_attribute(attr_name)
false
else
column = self.class.columns_hash[attr_name]
if column.nil?
if Numeric === value || value !~ /[^0-9]/
!value.to_i.zero?
else
!value.blank?
end
elsif column.number?
!value.zero?
else
!value.blank?
end
end
end
# A Person object with a name attribute can ask <tt>person.respond_to?("name")</tt>,
# <tt>person.respond_to?("name=")</tt>, and <tt>person.respond_to?("name?")</tt>
# which will all return +true+.
alias :respond_to_without_attributes? :respond_to?
def respond_to?(method, include_priv = false)
method_name = method.to_s
if super
return true
elsif !self.class.generated_methods?
self.class.define_attribute_methods
if self.class.generated_methods.include?(method_name)
return true
end
end
if @attributes.nil?
return super
elsif @attributes.include?(method_name)
return true
elsif md = self.class.match_attribute_method?(method_name)
return true if @attributes.include?(md.pre_match)
end
super
end
private
def missing_attribute(attr_name, stack)
raise ActiveRecord::MissingAttributeError, "missing attribute: #{attr_name}", stack
end
# Handle *? for method_missing.
def attribute?(attribute_name)
query_attribute(attribute_name)
end
# Handle *= for method_missing.
def attribute=(attribute_name, value)
write_attribute(attribute_name, value)
end
# Handle *_before_type_cast for method_missing.
def attribute_before_type_cast(attribute_name)
read_attribute_before_type_cast(attribute_name)
end
end
end

File diff suppressed because it is too large Load diff

View file

@ -1,275 +0,0 @@
module ActiveRecord
module Calculations #:nodoc:
CALCULATIONS_OPTIONS = [:conditions, :joins, :order, :select, :group, :having, :distinct, :limit, :offset, :include]
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
# Count operates using three different approaches.
#
# * Count all: By not passing any parameters to count, it will return a count of all the rows for the model.
# * Count using column: By passing a column name to count, it will return a count of all the rows for the model with supplied column present
# * Count using options will find the row count matched by the options used.
#
# The third approach, count using options, accepts an option hash as the only parameter. The options are:
#
# * <tt>:conditions</tt>: An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. See conditions in the intro.
# * <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 INNER JOIN on the associated table(s).
# 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>: Named associations that should be loaded alongside using LEFT OUTER JOINs. The symbols named refer
# to already defined associations. When using named associations, count returns the number of DISTINCT items for the model you're counting.
# See eager loading under Associations.
# * <tt>:order</tt>: An SQL fragment like "created_at DESC, name" (really only used with GROUP BY calculations).
# * <tt>:group</tt>: An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause.
# * <tt>:select</tt>: By default, this is * as in SELECT * FROM, but can be changed if you, for example, want to do a join but not
# include the joined columns.
# * <tt>:distinct</tt>: Set this to true to make this a distinct calculation, such as SELECT COUNT(DISTINCT posts.id) ...
#
# Examples for counting all:
# Person.count # returns the total count of all people
#
# Examples for counting by column:
# Person.count(:age) # returns the total count of all people whose age is present in database
#
# Examples for count with options:
# Person.count(:conditions => "age > 26")
# Person.count(:conditions => "age > 26 AND job.salary > 60000", :include => :job) # because of the named association, it finds the DISTINCT count using LEFT OUTER JOIN.
# Person.count(:conditions => "age > 26 AND job.salary > 60000", :joins => "LEFT JOIN jobs on jobs.person_id = person.id") # finds the number of rows matching the conditions and joins.
# Person.count('id', :conditions => "age > 26") # Performs a COUNT(id)
# Person.count(:all, :conditions => "age > 26") # Performs a COUNT(*) (:all is an alias for '*')
#
# Note: <tt>Person.count(:all)</tt> will not work because it will use <tt>:all</tt> as the condition. Use Person.count instead.
def count(*args)
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.
#
# Person.average('age')
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.
#
# Person.minimum('age')
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.
#
# Person.maximum('age')
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.
#
# Person.sum('age')
def sum(column_name, options = {})
calculate(:sum, column_name, options) || 0
end
# This calculates aggregate values in the given column. Methods for count, sum, average, minimum, and maximum have been added as shortcuts.
# Options such as <tt>:conditions</tt>, <tt>:order</tt>, <tt>:group</tt>, <tt>:having</tt>, and <tt>:joins</tt> can be passed to customize the query.
#
# There are two basic forms of output:
# * Single aggregate value: The single value is type cast to Fixnum for COUNT, Float for AVG, and the given column's type for everything else.
# * Grouped values: This returns an ordered hash of the values and groups them by the <tt>:group</tt> option. It takes either a column name, or the name
# of a belongs_to association.
#
# values = Person.maximum(:age, :group => 'last_name')
# puts values["Drake"]
# => 43
#
# drake = Family.find_by_last_name('Drake')
# values = Person.maximum(:age, :group => :family) # Person belongs_to :family
# puts values[drake]
# => 43
#
# values.each do |family, max_age|
# ...
# end
#
# Options:
# * <tt>:conditions</tt> - An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. See conditions in the intro.
# * <tt>:include</tt>: Eager loading, see Associations for details. Since calculations don't load anything, the purpose of this is to access fields on joined tables in your conditions, order, or group clauses.
# * <tt>:joins</tt> - An SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id". (Rarely needed).
# The records will be returned read-only since they will have attributes that do not correspond to the table's columns.
# * <tt>:order</tt> - An SQL fragment like "created_at DESC, name" (really only used with GROUP BY calculations).
# * <tt>:group</tt> - An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause.
# * <tt>:select</tt> - By default, this is * as in SELECT * FROM, but can be changed if you for example want to do a join, but not
# include the joined columns.
# * <tt>:distinct</tt> - Set this to true to make this a distinct calculation, such as SELECT COUNT(DISTINCT posts.id) ...
#
# Examples:
# Person.calculate(:count, :all) # The same as Person.count
# Person.average(:age) # SELECT AVG(age) FROM people...
# Person.minimum(:age, :conditions => ['last_name != ?', 'Drake']) # Selects the minimum age for everyone with a last name other than 'Drake'
# Person.minimum(:age, :having => 'min(age) > 17', :group => :last_name) # Selects the minimum age for any family without any minors
# Person.sum("2 * age")
def calculate(operation, column_name, options = {})
validate_calculation_options(operation, options)
column_name = options[:select] if options[:select]
column_name = '*' if column_name == :all
column = column_for column_name
catch :invalid_query do
if options[:group]
return execute_grouped_calculation(operation, column_name, column, options)
else
return execute_simple_calculation(operation, column_name, column, options)
end
end
0
end
protected
def construct_count_options_from_args(*args)
options = {}
column_name = :all
# We need to handle
# count()
# count(:column_name=:all)
# count(options={})
# count(column_name=:all, options={})
case args.size
when 1
args[0].is_a?(Hash) ? options = args[0] : column_name = args[0]
when 2
column_name, options = args
else
raise ArgumentError, "Unexpected parameters passed to count(): #{args.inspect}"
end if args.size > 0
[column_name, options]
end
def construct_calculation_sql(operation, column_name, options) #:nodoc:
operation = operation.to_s.downcase
options = options.symbolize_keys
scope = scope(:find)
merged_includes = merge_includes(scope ? scope[:include] : [], options[:include])
aggregate_alias = column_alias_for(operation, column_name)
column_name = "#{connection.quote_table_name(table_name)}.#{column_name}" if column_names.include?(column_name.to_s)
if operation == 'count'
if merged_includes.any?
options[:distinct] = true
column_name = options[:select] || [connection.quote_table_name(table_name), primary_key] * '.'
end
if options[:distinct]
use_workaround = !connection.supports_count_distinct?
end
end
if options[:distinct] && column_name.to_s !~ /\s*DISTINCT\s+/i
distinct = 'DISTINCT '
end
sql = "SELECT #{operation}(#{distinct}#{column_name}) AS #{aggregate_alias}"
# A (slower) workaround if we're using a backend, like sqlite, that doesn't support COUNT DISTINCT.
sql = "SELECT COUNT(*) AS #{aggregate_alias}" if use_workaround
sql << ", #{options[:group_field]} AS #{options[:group_alias]}" if options[:group]
sql << " FROM (SELECT #{distinct}#{column_name}" if use_workaround
sql << " FROM #{connection.quote_table_name(table_name)} "
if merged_includes.any?
join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, merged_includes, options[:joins])
sql << join_dependency.join_associations.collect{|join| join.association_join }.join
end
add_joins!(sql, options, scope)
add_conditions!(sql, options[:conditions], scope)
add_limited_ids_condition!(sql, options, join_dependency) if join_dependency && !using_limitable_reflections?(join_dependency.reflections) && ((scope && scope[:limit]) || options[:limit])
if options[:group]
group_key = connection.adapter_name == 'FrontBase' ? :group_alias : :group_field
sql << " GROUP BY #{options[group_key]} "
end
if options[:group] && options[:having]
# FrontBase requires identifiers in the HAVING clause and chokes on function calls
if connection.adapter_name == 'FrontBase'
options[:having].downcase!
options[:having].gsub!(/#{operation}\s*\(\s*#{column_name}\s*\)/, aggregate_alias)
end
sql << " HAVING #{options[:having]} "
end
sql << " ORDER BY #{options[:order]} " if options[:order]
add_limit!(sql, options, scope)
sql << ')' if use_workaround
sql
end
def execute_simple_calculation(operation, column_name, column, options) #:nodoc:
value = connection.select_value(construct_calculation_sql(operation, column_name, options))
type_cast_calculated_value(value, column, operation)
end
def execute_grouped_calculation(operation, column_name, column, options) #:nodoc:
group_attr = options[:group].to_s
association = reflect_on_association(group_attr.to_sym)
associated = association && association.macro == :belongs_to # only count belongs_to associations
group_field = associated ? association.primary_key_name : group_attr
group_alias = column_alias_for(group_field)
group_column = column_for group_field
sql = construct_calculation_sql(operation, column_name, options.merge(:group_field => group_field, :group_alias => group_alias))
calculated_data = connection.select_all(sql)
aggregate_alias = column_alias_for(operation, column_name)
if association
key_ids = calculated_data.collect { |row| row[group_alias] }
key_records = association.klass.base_class.find(key_ids)
key_records = key_records.inject({}) { |hsh, r| hsh.merge(r.id => r) }
end
calculated_data.inject(ActiveSupport::OrderedHash.new) do |all, row|
key = type_cast_calculated_value(row[group_alias], group_column)
key = key_records[key] if associated
value = row[aggregate_alias]
all[key] = type_cast_calculated_value(value, column, operation)
all
end
end
private
def validate_calculation_options(operation, options = {})
options.assert_valid_keys(CALCULATIONS_OPTIONS)
end
# Converts the given keys to the value that the database adapter returns as
# a usable column name:
#
# column_alias_for("users.id") # => "users_id"
# column_alias_for("sum(id)") # => "sum_id"
# column_alias_for("count(distinct users.id)") # => "count_distinct_users_id"
# column_alias_for("count(*)") # => "count_all"
# column_alias_for("count", "id") # => "count_id"
def column_alias_for(*keys)
connection.table_alias_for(keys.join(' ').downcase.gsub(/\*/, 'all').gsub(/\W+/, ' ').strip.gsub(/ +/, '_'))
end
def column_for(field)
field_name = field.to_s.split('.').last
columns.detect { |c| c.name.to_s == field_name }
end
def type_cast_calculated_value(value, column, operation = nil)
operation = operation.to_s.downcase
case operation
when 'count' then value.to_i
when 'avg' then value && value.to_f
else column ? column.type_cast(value) : value
end
end
end
end
end

View file

@ -1,312 +0,0 @@
require 'observer'
module ActiveRecord
# Callbacks are hooks into the lifecycle of an Active Record object that allow you to trigger logic
# before or after an alteration of the object state. This can be used to make sure that associated and
# dependent objects are deleted when destroy is called (by overwriting +before_destroy+) or to massage attributes
# before they're validated (by overwriting +before_validation+). As an example of the callbacks initiated, consider
# the <tt>Base#save</tt> call:
#
# * (-) <tt>save</tt>
# * (-) <tt>valid</tt>
# * (1) <tt>before_validation</tt>
# * (2) <tt>before_validation_on_create</tt>
# * (-) <tt>validate</tt>
# * (-) <tt>validate_on_create</tt>
# * (3) <tt>after_validation</tt>
# * (4) <tt>after_validation_on_create</tt>
# * (5) <tt>before_save</tt>
# * (6) <tt>before_create</tt>
# * (-) <tt>create</tt>
# * (7) <tt>after_create</tt>
# * (8) <tt>after_save</tt>
#
# That's a total of eight callbacks, which gives you immense power to react and prepare for each state in the
# Active Record lifecycle.
#
# Examples:
# class CreditCard < ActiveRecord::Base
# # Strip everything but digits, so the user can specify "555 234 34" or
# # "5552-3434" or both will mean "55523434"
# def before_validation_on_create
# self.number = number.gsub(/[^0-9]/, "") if attribute_present?("number")
# end
# end
#
# class Subscription < ActiveRecord::Base
# before_create :record_signup
#
# private
# def record_signup
# self.signed_up_on = Date.today
# end
# end
#
# class Firm < ActiveRecord::Base
# # Destroys the associated clients and people when the firm is destroyed
# before_destroy { |record| Person.destroy_all "firm_id = #{record.id}" }
# before_destroy { |record| Client.destroy_all "client_of = #{record.id}" }
# end
#
# == Inheritable callback queues
#
# Besides the overwriteable callback methods, it's also possible to register callbacks through the use of the callback macros.
# Their main advantage is that the macros add behavior into a callback queue that is kept intact down through an inheritance
# hierarchy. Example:
#
# class Topic < ActiveRecord::Base
# before_destroy :destroy_author
# end
#
# class Reply < Topic
# before_destroy :destroy_readers
# end
#
# Now, when <tt>Topic#destroy</tt> is run only +destroy_author+ is called. When <tt>Reply#destroy</tt> is run, both +destroy_author+ and
# +destroy_readers+ are called. Contrast this to the situation where we've implemented the save behavior through overwriteable
# methods:
#
# class Topic < ActiveRecord::Base
# def before_destroy() destroy_author end
# end
#
# class Reply < Topic
# def before_destroy() destroy_readers end
# end
#
# 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.
#
# *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
# be inherited.
#
# == Types of callbacks
#
# There are four types of callbacks accepted by the callback macros: Method references (symbol), callback objects,
# inline methods (using a proc), and inline eval methods (using a string). Method references and callback objects are the
# recommended approaches, inline methods using a proc are sometimes appropriate (such as for creating mix-ins), and inline
# eval methods are deprecated.
#
# The method reference callbacks work by specifying a protected or private method available in the object, like this:
#
# class Topic < ActiveRecord::Base
# before_destroy :delete_parents
#
# private
# def delete_parents
# self.class.delete_all "parent_id = #{id}"
# end
# end
#
# The callback objects have methods named after the callback called with the record as the only parameter, such as:
#
# class BankAccount < ActiveRecord::Base
# before_save EncryptionWrapper.new("credit_card_number")
# after_save EncryptionWrapper.new("credit_card_number")
# after_initialize EncryptionWrapper.new("credit_card_number")
# end
#
# class EncryptionWrapper
# def initialize(attribute)
# @attribute = attribute
# end
#
# def before_save(record)
# record.credit_card_number = encrypt(record.credit_card_number)
# end
#
# def after_save(record)
# record.credit_card_number = decrypt(record.credit_card_number)
# end
#
# alias_method :after_find, :after_save
#
# private
# def encrypt(value)
# # Secrecy is committed
# end
#
# def decrypt(value)
# # Secrecy is unveiled
# end
# end
#
# So you specify the object you want messaged on a given callback. When that callback is triggered, the object has
# a method by the name of the callback messaged.
#
# The callback macros usually accept a symbol for the method they're supposed to run, but you can also pass a "method string",
# which will then be evaluated within the binding of the callback. Example:
#
# class Topic < ActiveRecord::Base
# before_destroy 'self.class.delete_all "parent_id = #{id}"'
# end
#
# Notice that single quotes (') are used so the <tt>#{id}</tt> part isn't evaluated until the callback is triggered. Also note that these
# inline callbacks can be stacked just like the regular ones:
#
# class Topic < ActiveRecord::Base
# before_destroy 'self.class.delete_all "parent_id = #{id}"',
# 'puts "Evaluated after parents are destroyed"'
# end
#
# == The +after_find+ and +after_initialize+ exceptions
#
# Because +after_find+ and +after_initialize+ are called for each object found and instantiated by a finder, such as <tt>Base.find(:all)</tt>, we've had
# to implement a simple performance constraint (50% more speed on a simple test case). Unlike all the other callbacks, +after_find+ and
# +after_initialize+ will only be run if an explicit implementation is defined (<tt>def after_find</tt>). In that case, all of the
# callback types will be called.
#
# == <tt>before_validation*</tt> returning statements
#
# If the returning value of a +before_validation+ callback can be evaluated to +false+, the process will be aborted and <tt>Base#save</tt> will return +false+.
# If Base#save! is called it will raise a RecordNotSaved exception.
# Nothing will be appended to the errors object.
#
# == Canceling callbacks
#
# If a <tt>before_*</tt> callback returns +false+, all the later callbacks and the associated action are cancelled. If an <tt>after_*</tt> callback returns
# +false+, all the later callbacks are cancelled. Callbacks are generally run in the order they are defined, with the exception of callbacks
# defined as methods on the model, which are called last.
module Callbacks
CALLBACKS = %w(
after_find after_initialize before_save after_save before_create after_create before_update after_update before_validation
after_validation before_validation_on_create after_validation_on_create before_validation_on_update
after_validation_on_update before_destroy after_destroy
)
def self.included(base) #:nodoc:
base.extend Observable
[:create_or_update, :valid?, :create, :update, :destroy].each do |method|
base.send :alias_method_chain, method, :callbacks
end
base.send :include, ActiveSupport::Callbacks
base.define_callbacks *CALLBACKS
end
# Is called when the object was instantiated by one of the finders, like <tt>Base.find</tt>.
#def after_find() end
# Is called after the object has been instantiated by a call to <tt>Base.new</tt>.
#def after_initialize() end
# Is called _before_ <tt>Base.save</tt> (regardless of whether it's a +create+ or +update+ save).
def before_save() end
# Is called _after_ <tt>Base.save</tt> (regardless of whether it's a +create+ or +update+ save).
#
# class Contact < ActiveRecord::Base
# after_save { logger.info( 'New contact saved!' ) }
# end
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)
result
end
private :create_or_update_with_callbacks
# Is called _before_ <tt>Base.save</tt> on new objects that haven't been saved yet (no record exists).
def before_create() end
# Is called _after_ <tt>Base.save</tt> on new objects that haven't been saved yet (no record exists).
def after_create() end
def create_with_callbacks #:nodoc:
return false if callback(:before_create) == false
result = create_without_callbacks
callback(:after_create)
result
end
private :create_with_callbacks
# Is called _before_ <tt>Base.save</tt> on existing objects that have a record.
def before_update() end
# Is called _after_ <tt>Base.save</tt> on existing objects that have a record.
def after_update() end
def update_with_callbacks(*args) #:nodoc:
return false if callback(:before_update) == false
result = update_without_callbacks(*args)
callback(:after_update)
result
end
private :update_with_callbacks
# Is called _before_ <tt>Validations.validate</tt> (which is part of the <tt>Base.save</tt> call).
def before_validation() end
# Is called _after_ <tt>Validations.validate</tt> (which is part of the <tt>Base.save</tt> call).
def after_validation() end
# Is called _before_ <tt>Validations.validate</tt> (which is part of the <tt>Base.save</tt> call) on new objects
# that haven't been saved yet (no record exists).
def before_validation_on_create() end
# Is called _after_ <tt>Validations.validate</tt> (which is part of the <tt>Base.save</tt> call) on new objects
# that haven't been saved yet (no record exists).
def after_validation_on_create() end
# Is called _before_ <tt>Validations.validate</tt> (which is part of the <tt>Base.save</tt> call) on
# existing objects that have a record.
def before_validation_on_update() end
# Is called _after_ <tt>Validations.validate</tt> (which is part of the <tt>Base.save</tt> call) on
# existing objects that have a record.
def after_validation_on_update() end
def valid_with_callbacks? #:nodoc:
return false if callback(:before_validation) == false
if new_record? then result = callback(:before_validation_on_create) else result = callback(:before_validation_on_update) end
return false if result == false
result = valid_without_callbacks?
callback(:after_validation)
if new_record? then callback(:after_validation_on_create) else callback(:after_validation_on_update) end
return result
end
# Is called _before_ <tt>Base.destroy</tt>.
#
# Note: If you need to _destroy_ or _nullify_ associated records first,
# use the <tt>:dependent</tt> option on your associations.
def before_destroy() end
# Is called _after_ <tt>Base.destroy</tt> (and all the attributes have been frozen).
#
# class Contact < ActiveRecord::Base
# after_destroy { |record| logger.info( "Contact #{record.id} was destroyed." ) }
# end
def after_destroy() end
def destroy_with_callbacks #:nodoc:
return false if callback(:before_destroy) == false
result = destroy_without_callbacks
callback(:after_destroy)
result
end
private
def callback(method)
notify(method)
result = run_callbacks(method) { |result, object| result == false }
if result != false && respond_to_without_attributes?(method)
result = send(method)
end
return result
end
def notify(method) #:nodoc:
self.class.changed
self.class.notify_observers(method, self)
end
end
end

View file

@ -1,309 +0,0 @@
require 'set'
module ActiveRecord
class Base
class ConnectionSpecification #:nodoc:
attr_reader :config, :adapter_method
def initialize (config, adapter_method)
@config, @adapter_method = config, adapter_method
end
end
# Check for activity after at least +verification_timeout+ seconds.
# Defaults to 0 (always check.)
cattr_accessor :verification_timeout, :instance_writer => false
@@verification_timeout = 0
# The class -> [adapter_method, config] map
@@defined_connections = {}
# The class -> thread id -> adapter cache. (class -> adapter if not allow_concurrency)
@@active_connections = {}
class << self
# Retrieve the connection cache.
def thread_safe_active_connections #:nodoc:
@@active_connections[Thread.current.object_id] ||= {}
end
def single_threaded_active_connections #:nodoc:
@@active_connections
end
# pick up the right active_connection method from @@allow_concurrency
if @@allow_concurrency
alias_method :active_connections, :thread_safe_active_connections
else
alias_method :active_connections, :single_threaded_active_connections
end
# set concurrency support flag (not thread safe, like most of the methods in this file)
def allow_concurrency=(threaded) #:nodoc:
logger.debug "allow_concurrency=#{threaded}" if logger
return if @@allow_concurrency == threaded
clear_all_cached_connections!
@@allow_concurrency = threaded
method_prefix = threaded ? "thread_safe" : "single_threaded"
sing = (class << self; self; end)
[:active_connections, :scoped_methods].each do |method|
sing.send(:alias_method, method, "#{method_prefix}_#{method}")
end
log_connections if logger
end
def active_connection_name #:nodoc:
@active_connection_name ||=
if active_connections[name] || @@defined_connections[name]
name
elsif self == ActiveRecord::Base
nil
else
superclass.active_connection_name
end
end
def clear_active_connection_name #:nodoc:
@active_connection_name = nil
subclasses.each { |klass| klass.clear_active_connection_name }
end
# Returns the connection currently associated with the class. This can
# also be used to "borrow" the connection to do database work unrelated
# to any of the specific Active Records.
def connection
if defined?(@active_connection_name) && (conn = active_connections[@active_connection_name])
conn
else
# retrieve_connection sets the cache key.
conn = retrieve_connection
active_connections[@active_connection_name] = conn
end
end
# Clears the cache which maps classes to connections.
def clear_active_connections!
clear_cache!(@@active_connections) do |name, conn|
conn.disconnect!
end
end
# Clears the cache which maps classes
def clear_reloadable_connections!
if @@allow_concurrency
# With concurrent connections @@active_connections is
# a hash keyed by thread id.
@@active_connections.each do |thread_id, conns|
conns.each do |name, conn|
if conn.requires_reloading?
conn.disconnect!
@@active_connections[thread_id].delete(name)
end
end
end
else
@@active_connections.each do |name, conn|
if conn.requires_reloading?
conn.disconnect!
@@active_connections.delete(name)
end
end
end
end
# Verify active connections.
def verify_active_connections! #:nodoc:
if @@allow_concurrency
remove_stale_cached_threads!(@@active_connections) do |name, conn|
conn.disconnect!
end
end
active_connections.each_value do |connection|
connection.verify!(@@verification_timeout)
end
end
private
def clear_cache!(cache, thread_id = nil, &block)
if cache
if @@allow_concurrency
thread_id ||= Thread.current.object_id
thread_cache, cache = cache, cache[thread_id]
return unless cache
end
cache.each(&block) if block_given?
cache.clear
end
ensure
if thread_cache && @@allow_concurrency
thread_cache.delete(thread_id)
end
end
# Remove stale threads from the cache.
def remove_stale_cached_threads!(cache, &block)
stale = Set.new(cache.keys)
Thread.list.each do |thread|
stale.delete(thread.object_id) if thread.alive?
end
stale.each do |thread_id|
clear_cache!(cache, thread_id, &block)
end
end
def clear_all_cached_connections!
if @@allow_concurrency
@@active_connections.each_value do |connection_hash_for_thread|
connection_hash_for_thread.each_value {|conn| conn.disconnect! }
connection_hash_for_thread.clear
end
else
@@active_connections.each_value {|conn| conn.disconnect! }
end
@@active_connections.clear
end
end
# Returns the connection currently associated with the class. This can
# also be used to "borrow" the connection to do database work that isn't
# easily done without going straight to SQL.
def connection
self.class.connection
end
# Establishes the connection to the database. Accepts a hash as input where
# the <tt>:adapter</tt> key must be specified with the name of a database adapter (in lower-case)
# example for regular databases (MySQL, Postgresql, etc):
#
# ActiveRecord::Base.establish_connection(
# :adapter => "mysql",
# :host => "localhost",
# :username => "myuser",
# :password => "mypass",
# :database => "somedatabase"
# )
#
# Example for SQLite database:
#
# ActiveRecord::Base.establish_connection(
# :adapter => "sqlite",
# :database => "path/to/dbfile"
# )
#
# Also accepts keys as strings (for parsing from YAML for example):
#
# ActiveRecord::Base.establish_connection(
# "adapter" => "sqlite",
# "database" => "path/to/dbfile"
# )
#
# The exceptions AdapterNotSpecified, AdapterNotFound and ArgumentError
# may be returned on an error.
def self.establish_connection(spec = nil)
case spec
when nil
raise AdapterNotSpecified unless defined? RAILS_ENV
establish_connection(RAILS_ENV)
when ConnectionSpecification
clear_active_connection_name
@active_connection_name = name
@@defined_connections[name] = spec
when Symbol, String
if configuration = configurations[spec.to_s]
establish_connection(configuration)
else
raise AdapterNotSpecified, "#{spec} database is not configured"
end
else
spec = spec.symbolize_keys
unless spec.key?(:adapter) then raise AdapterNotSpecified, "database configuration does not specify adapter" end
begin
require 'rubygems'
gem "activerecord-#{spec[:adapter]}-adapter"
require "active_record/connection_adapters/#{spec[:adapter]}_adapter"
rescue LoadError
begin
require "active_record/connection_adapters/#{spec[:adapter]}_adapter"
rescue LoadError
raise "Please install the #{spec[:adapter]} adapter: `gem install activerecord-#{spec[:adapter]}-adapter` (#{$!})"
end
end
adapter_method = "#{spec[:adapter]}_connection"
if !respond_to?(adapter_method)
raise AdapterNotFound, "database configuration specifies nonexistent #{spec[:adapter]} adapter"
end
remove_connection
establish_connection(ConnectionSpecification.new(spec, adapter_method))
end
end
# Locate the connection of the nearest super class. This can be an
# active or defined connection: if it is the latter, it will be
# opened and set as the active connection for the class it was defined
# for (not necessarily the current class).
def self.retrieve_connection #:nodoc:
# Name is nil if establish_connection hasn't been called for
# some class along the inheritance chain up to AR::Base yet.
if name = active_connection_name
if conn = active_connections[name]
# Verify the connection.
conn.verify!(@@verification_timeout)
elsif spec = @@defined_connections[name]
# Activate this connection specification.
klass = name.constantize
klass.connection = spec
conn = active_connections[name]
end
end
conn or raise ConnectionNotEstablished
end
# Returns true if a connection that's accessible to this class has already been opened.
def self.connected?
active_connections[active_connection_name] ? true : false
end
# Remove the connection for this class. This will close the active
# connection and the defined connection (if they exist). The result
# can be used as an argument for establish_connection, for easily
# re-establishing the connection.
def self.remove_connection(klass=self)
spec = @@defined_connections[klass.name]
konn = active_connections[klass.name]
@@defined_connections.delete_if { |key, value| value == spec }
active_connections.delete_if { |key, value| value == konn }
konn.disconnect! if konn
spec.config if spec
end
# Set the connection for the class.
def self.connection=(spec) #:nodoc:
if spec.kind_of?(ActiveRecord::ConnectionAdapters::AbstractAdapter)
active_connections[name] = spec
elsif spec.kind_of?(ConnectionSpecification)
config = spec.config.reverse_merge(:allow_concurrency => @@allow_concurrency)
self.connection = self.send(spec.adapter_method, config)
elsif spec.nil?
raise ConnectionNotEstablished
else
establish_connection spec
end
end
# connection state logging
def self.log_connections #:nodoc:
if logger
logger.info "Defined connections: #{@@defined_connections.inspect}"
logger.info "Active connections: #{active_connections.inspect}"
logger.info "Active connection name: #{@active_connection_name}"
end
end
end
end

View file

@ -1,176 +0,0 @@
module ActiveRecord
module ConnectionAdapters # :nodoc:
module DatabaseStatements
# Returns an array of record hashes with the column names as keys and
# column values as values.
def select_all(sql, name = nil)
select(sql, name)
end
# Returns a record hash with the column names as keys and column values
# as values.
def select_one(sql, name = nil)
result = select_all(sql, name)
result.first if result
end
# Returns a single value from a record
def select_value(sql, name = nil)
if result = select_one(sql, name)
result.values.first
end
end
# Returns an array of the values of the first column in a select:
# select_values("SELECT id FROM companies LIMIT 3") => [1,2,3]
def select_values(sql, name = nil)
result = select_rows(sql, name)
result.map { |v| v[0] }
end
# 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
# Executes the SQL statement in the context of this connection.
def execute(sql, name = nil)
raise NotImplementedError, "execute is an abstract method"
end
# Returns the last auto-generated ID from the affected table.
def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
insert_sql(sql, name, pk, id_value, sequence_name)
end
# Executes the update statement and returns the number of rows affected.
def update(sql, name = nil)
update_sql(sql, name)
end
# Executes the delete statement and returns the number of rows affected.
def delete(sql, name = nil)
delete_sql(sql, name)
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_open = false
rollback_db_transaction
end
raise unless database_transaction_rollback.is_a? ActiveRecord::Rollback
end
ensure
if transaction_open
begin
commit_db_transaction
rescue Exception => database_transaction_rollback
rollback_db_transaction
raise
end
end
end
# Begins the transaction (and turns off auto-committing).
def begin_db_transaction() end
# Commits the transaction (and turns on auto-committing).
def commit_db_transaction() end
# Rolls back the transaction (and turns on auto-committing). Must be
# done if the transaction block raises an exception or returns false.
def rollback_db_transaction() end
# Alias for <tt>add_limit_offset!</tt>.
def add_limit!(sql, options)
add_limit_offset!(sql, options) if options
end
# Appends +LIMIT+ and +OFFSET+ options to an SQL statement.
# This method *modifies* the +sql+ parameter.
# ===== Examples
# add_limit_offset!('SELECT * FROM suppliers', {:limit => 10, :offset => 50})
# generates
# SELECT * FROM suppliers LIMIT 10 OFFSET 50
def add_limit_offset!(sql, options)
if limit = options[:limit]
sql << " LIMIT #{sanitize_limit(limit)}"
if offset = options[:offset]
sql << " OFFSET #{offset.to_i}"
end
end
sql
end
def sanitize_limit(limit)
limit.to_s[/,/] ? limit.split(',').map{ |i| i.to_i }.join(',') : limit.to_i
end
# Appends a locking clause to an SQL statement.
# This method *modifies* the +sql+ parameter.
# # SELECT * FROM suppliers FOR UPDATE
# add_lock! 'SELECT * FROM suppliers', :lock => true
# add_lock! 'SELECT * FROM suppliers', :lock => ' FOR UPDATE'
def add_lock!(sql, options)
case lock = options[:lock]
when true; sql << ' FOR UPDATE'
when String; sql << " #{lock}"
end
end
def default_sequence_name(table, column)
nil
end
# Set the sequence to the max value of the table's column.
def reset_sequence!(table, column, sequence = nil)
# Do nothing by default. Implement for PostgreSQL, Oracle, ...
end
# Inserts the given fixture into the table. Overridden in adapters that require
# something beyond a simple insert (eg. Oracle).
def insert_fixture(fixture, table_name)
execute "INSERT INTO #{quote_table_name(table_name)} (#{fixture.key_list}) VALUES (#{fixture.value_list})", 'Fixture Insert'
end
def empty_insert_statement(table_name)
"INSERT INTO #{quote_table_name(table_name)} VALUES(DEFAULT)"
end
protected
# 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
# Returns the last auto-generated ID from the affected table.
def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
execute(sql, name)
id_value
end
# Executes the update statement and returns the number of rows affected.
def update_sql(sql, name = nil)
execute(sql, name)
end
# Executes the delete statement and returns the number of rows affected.
def delete_sql(sql, name = nil)
update_sql(sql, name)
end
end
end
end

View file

@ -1,93 +0,0 @@
module ActiveRecord
module ConnectionAdapters # :nodoc:
module QueryCache
class << self
def included(base)
base.class_eval do
attr_accessor :query_cache_enabled
alias_method_chain :columns, :query_cache
alias_method_chain :select_all, :query_cache
end
dirties_query_cache base, :insert, :update, :delete
end
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
end_code
end
end
end
# Enable the query cache within the block.
def cache
old, @query_cache_enabled = @query_cache_enabled, true
@query_cache ||= {}
yield
ensure
clear_query_cache
@query_cache_enabled = old
end
# Disable the query cache within the block.
def uncached
old, @query_cache_enabled = @query_cache_enabled, false
yield
ensure
@query_cache_enabled = old
end
# Clears the query cache.
#
# One reason you may wish to call this method explicitly is between queries
# that ask the database to randomize results. Otherwise the cache would see
# the same SQL query and repeatedly return the same result each time, silently
# undermining the randomness you were expecting.
def clear_query_cache
@query_cache.clear if @query_cache
end
def select_all_with_query_cache(*args)
if @query_cache_enabled
cache_sql(args.first) { select_all_without_query_cache(*args) }
else
select_all_without_query_cache(*args)
end
end
def columns_with_query_cache(*args)
if @query_cache_enabled
@query_cache["SHOW FIELDS FROM #{args.first}"] ||= columns_without_query_cache(*args)
else
columns_without_query_cache(*args)
end
end
private
def cache_sql(sql)
result =
if @query_cache.has_key?(sql)
log_info(sql, "CACHE", 0.0)
@query_cache[sql]
else
@query_cache[sql] = yield
end
if Array === result
result.collect { |row| row.dup }
else
result.duplicable? ? result.dup : result
end
rescue TypeError
result
end
end
end
end

View file

@ -1,69 +0,0 @@
module ActiveRecord
module ConnectionAdapters # :nodoc:
module Quoting
# Quotes the column value to help prevent
# {SQL injection attacks}[http://en.wikipedia.org/wiki/SQL_injection].
def quote(value, column = nil)
# records are quoted as their primary key
return value.quoted_id if value.respond_to?(:quoted_id)
case value
when String, ActiveSupport::Multibyte::Chars
value = value.to_s
if column && column.type == :binary && column.class.respond_to?(:string_to_binary)
"#{quoted_string_prefix}'#{quote_string(column.class.string_to_binary(value))}'" # ' (for ruby-mode)
elsif column && [:integer, :float].include?(column.type)
value = column.type == :integer ? value.to_i : value.to_f
value.to_s
else
"#{quoted_string_prefix}'#{quote_string(value)}'" # ' (for ruby-mode)
end
when NilClass then "NULL"
when TrueClass then (column && column.type == :integer ? '1' : quoted_true)
when FalseClass then (column && column.type == :integer ? '0' : quoted_false)
when Float, Fixnum, Bignum then value.to_s
# BigDecimals need to be output in a non-normalized form and quoted.
when BigDecimal then value.to_s('F')
else
if value.acts_like?(:date) || value.acts_like?(:time)
"'#{quoted_date(value)}'"
else
"#{quoted_string_prefix}'#{quote_string(value.to_yaml)}'"
end
end
end
# Quotes a string, escaping any ' (single quote) and \ (backslash)
# characters.
def quote_string(s)
s.gsub(/\\/, '\&\&').gsub(/'/, "''") # ' (for ruby-mode)
end
# Quotes the column name. Defaults to no quoting.
def quote_column_name(column_name)
column_name
end
# Quotes the table name. Defaults to column name quoting.
def quote_table_name(table_name)
quote_column_name(table_name)
end
def quoted_true
"'t'"
end
def quoted_false
"'f'"
end
def quoted_date(value)
value.to_s(:db)
end
def quoted_string_prefix
''
end
end
end
end

View file

@ -1,661 +0,0 @@
require 'date'
require 'bigdecimal'
require 'bigdecimal/util'
module ActiveRecord
module ConnectionAdapters #:nodoc:
# An abstract definition of a column in a table.
class Column
module Format
ISO_DATE = /\A(\d{4})-(\d\d)-(\d\d)\z/
ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/
end
attr_reader :name, :default, :type, :limit, :null, :sql_type, :precision, :scale
attr_accessor :primary
# Instantiates a new column in the table.
#
# +name+ is the column's name, such as <tt>supplier_id</tt> in <tt>supplier_id int(11)</tt>.
# +default+ is the type-casted default value, such as +new+ in <tt>sales_stage varchar(20) default 'new'</tt>.
# +sql_type+ is only used to extract the column's length, if necessary. For example +60+ in <tt>company_name varchar(60)</tt>.
# +null+ determines if this column allows +NULL+ values.
def initialize(name, default, sql_type = nil, null = true)
@name, @sql_type, @null = name, sql_type, null
@limit, @precision, @scale = extract_limit(sql_type), extract_precision(sql_type), extract_scale(sql_type)
@type = simplified_type(sql_type)
@default = extract_default(default)
@primary = nil
end
def text?
[:string, :text].include? type
end
def number?
[:float, :integer, :decimal].include? type
end
# Returns the Ruby class that corresponds to the abstract data type.
def klass
case type
when :integer then Fixnum
when :float then Float
when :decimal then BigDecimal
when :datetime then Time
when :date then Date
when :timestamp then Time
when :time then Time
when :text, :string then String
when :binary then String
when :boolean then Object
end
end
# Casts value (which is a String) to an appropriate instance.
def type_cast(value)
return nil if value.nil?
case type
when :string then value
when :text then value
when :integer then value.to_i rescue value ? 1 : 0
when :float then value.to_f
when :decimal then self.class.value_to_decimal(value)
when :datetime then self.class.string_to_time(value)
when :timestamp then self.class.string_to_time(value)
when :time then self.class.string_to_dummy_time(value)
when :date then self.class.string_to_date(value)
when :binary then self.class.binary_to_string(value)
when :boolean then self.class.value_to_boolean(value)
else value
end
end
def type_cast_code(var_name)
case type
when :string then nil
when :text then nil
when :integer then "(#{var_name}.to_i rescue #{var_name} ? 1 : 0)"
when :float then "#{var_name}.to_f"
when :decimal then "#{self.class.name}.value_to_decimal(#{var_name})"
when :datetime then "#{self.class.name}.string_to_time(#{var_name})"
when :timestamp then "#{self.class.name}.string_to_time(#{var_name})"
when :time then "#{self.class.name}.string_to_dummy_time(#{var_name})"
when :date then "#{self.class.name}.string_to_date(#{var_name})"
when :binary then "#{self.class.name}.binary_to_string(#{var_name})"
when :boolean then "#{self.class.name}.value_to_boolean(#{var_name})"
else nil
end
end
# Returns the human name of the column name.
#
# ===== Examples
# Column.new('sales_stage', ...).human_name # => 'Sales stage'
def human_name
Base.human_attribute_name(@name)
end
def extract_default(default)
type_cast(default)
end
class << self
# Used to convert from Strings to BLOBs
def string_to_binary(value)
value
end
# Used to convert from BLOBs to Strings
def binary_to_string(value)
value
end
def string_to_date(string)
return string unless string.is_a?(String)
return nil if string.empty?
fast_string_to_date(string) || fallback_string_to_date(string)
end
def string_to_time(string)
return string unless string.is_a?(String)
return nil if string.empty?
fast_string_to_time(string) || fallback_string_to_time(string)
end
def string_to_dummy_time(string)
return string unless string.is_a?(String)
return nil if string.empty?
string_to_time "2000-01-01 #{string}"
end
# convert something to a boolean
def value_to_boolean(value)
if value == true || value == false
value
else
%w(true t 1).include?(value.to_s.downcase)
end
end
# convert something to a BigDecimal
def value_to_decimal(value)
# Using .class is faster than .is_a? and
# subclasses of BigDecimal will be handled
# in the else clause
if value.class == BigDecimal
value
elsif value.respond_to?(:to_d)
value.to_d
else
value.to_s.to_d
end
end
protected
# '0.123456' -> 123456
# '1.123456' -> 123456
def microseconds(time)
((time[:sec_fraction].to_f % 1) * 1_000_000).to_i
end
def new_date(year, mon, mday)
if year && year != 0
Date.new(year, mon, mday) rescue nil
end
end
def new_time(year, mon, mday, hour, min, sec, microsec)
# Treat 0000-00-00 00:00:00 as nil.
return nil if year.nil? || year == 0
Time.time_with_datetime_fallback(Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil
end
def fast_string_to_date(string)
if string =~ Format::ISO_DATE
new_date $1.to_i, $2.to_i, $3.to_i
end
end
# Doesn't handle time zones.
def fast_string_to_time(string)
if string =~ Format::ISO_DATETIME
microsec = ($7.to_f * 1_000_000).to_i
new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec
end
end
def fallback_string_to_date(string)
new_date(*::Date._parse(string, false).values_at(:year, :mon, :mday))
end
def fallback_string_to_time(string)
time_hash = Date._parse(string)
time_hash[:sec_fraction] = microseconds(time_hash)
new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction))
end
end
private
def extract_limit(sql_type)
$1.to_i if sql_type =~ /\((.*)\)/
end
def extract_precision(sql_type)
$2.to_i if sql_type =~ /^(numeric|decimal|number)\((\d+)(,\d+)?\)/i
end
def extract_scale(sql_type)
case sql_type
when /^(numeric|decimal|number)\((\d+)\)/i then 0
when /^(numeric|decimal|number)\((\d+)(,(\d+))\)/i then $4.to_i
end
end
def simplified_type(field_type)
case field_type
when /int/i
:integer
when /float|double/i
:float
when /decimal|numeric|number/i
extract_scale(field_type) == 0 ? :integer : :decimal
when /datetime/i
:datetime
when /timestamp/i
:timestamp
when /time/i
:time
when /date/i
:date
when /clob/i, /text/i
:text
when /blob/i, /binary/i
:binary
when /char/i, /string/i
:string
when /boolean/i
:boolean
end
end
end
class IndexDefinition < Struct.new(:table, :name, :unique, :columns) #:nodoc:
end
class ColumnDefinition < Struct.new(:base, :name, :type, :limit, :precision, :scale, :default, :null) #:nodoc:
def sql_type
base.type_to_sql(type.to_sym, limit, precision, scale) rescue type
end
def to_sql
column_sql = "#{base.quote_column_name(name)} #{sql_type}"
add_column_options!(column_sql, :null => null, :default => default) unless type.to_sym == :primary_key
column_sql
end
alias to_s :to_sql
private
def add_column_options!(sql, options)
base.add_column_options!(sql, options.merge(:column => self))
end
end
# Represents a SQL table in an abstract way.
# Columns are stored as a ColumnDefinition in the +columns+ attribute.
class TableDefinition
attr_accessor :columns
def initialize(base)
@columns = []
@base = base
end
# Appends a primary key definition to the table definition.
# Can be called multiple times, but this is probably not a good idea.
def primary_key(name)
column(name, :primary_key)
end
# Returns a ColumnDefinition for the column with name +name+.
def [](name)
@columns.find {|column| column.name.to_s == name.to_s}
end
# Instantiates a new column for the table.
# The +type+ parameter is normally one of the migrations native types,
# which is one of the following:
# <tt>:primary_key</tt>, <tt>:string</tt>, <tt>:text</tt>,
# <tt>:integer</tt>, <tt>:float</tt>, <tt>:decimal</tt>,
# <tt>:datetime</tt>, <tt>:timestamp</tt>, <tt>:time</tt>,
# <tt>:date</tt>, <tt>:binary</tt>, <tt>:boolean</tt>.
#
# You may use a type not in this list as long as it is supported by your
# database (for example, "polygon" in MySQL), but this will not be database
# agnostic and should usually be avoided.
#
# Available options are (none of these exists by default):
# * <tt>:limit</tt> -
# Requests a maximum column length (<tt>:string</tt>, <tt>:text</tt>,
# <tt>:binary</tt> or <tt>:integer</tt> columns only)
# * <tt>:default</tt> -
# The column's default value. Use nil for NULL.
# * <tt>:null</tt> -
# Allows or disallows +NULL+ values in the column. This option could
# have been named <tt>:null_allowed</tt>.
# * <tt>:precision</tt> -
# Specifies the precision for a <tt>:decimal</tt> column.
# * <tt>:scale</tt> -
# Specifies the scale for a <tt>:decimal</tt> column.
#
# Please be aware of different RDBMS implementations behavior with
# <tt>:decimal</tt> columns:
# * The SQL standard says the default scale should be 0, <tt>:scale</tt> <=
# <tt>:precision</tt>, and makes no comments about the requirements of
# <tt>:precision</tt>.
# * MySQL: <tt>:precision</tt> [1..63], <tt>:scale</tt> [0..30].
# Default is (10,0).
# * PostgreSQL: <tt>:precision</tt> [1..infinity],
# <tt>:scale</tt> [0..infinity]. No default.
# * SQLite2: Any <tt>:precision</tt> and <tt>:scale</tt> may be used.
# Internal storage as strings. No default.
# * SQLite3: No restrictions on <tt>:precision</tt> and <tt>:scale</tt>,
# but the maximum supported <tt>:precision</tt> is 16. No default.
# * Oracle: <tt>:precision</tt> [1..38], <tt>:scale</tt> [-84..127].
# Default is (38,0).
# * DB2: <tt>:precision</tt> [1..63], <tt>:scale</tt> [0..62].
# Default unknown.
# * Firebird: <tt>:precision</tt> [1..18], <tt>:scale</tt> [0..18].
# Default (9,0). Internal types NUMERIC and DECIMAL have different
# storage rules, decimal being better.
# * FrontBase?: <tt>:precision</tt> [1..38], <tt>:scale</tt> [0..38].
# Default (38,0). WARNING Max <tt>:precision</tt>/<tt>:scale</tt> for
# NUMERIC is 19, and DECIMAL is 38.
# * SqlServer?: <tt>:precision</tt> [1..38], <tt>:scale</tt> [0..38].
# Default (38,0).
# * Sybase: <tt>:precision</tt> [1..38], <tt>:scale</tt> [0..38].
# Default (38,0).
# * OpenBase?: Documentation unclear. Claims storage in <tt>double</tt>.
#
# This method returns <tt>self</tt>.
#
# == Examples
# # Assuming td is an instance of TableDefinition
# td.column(:granted, :boolean)
# # granted BOOLEAN
#
# td.column(:picture, :binary, :limit => 2.megabytes)
# # => picture BLOB(2097152)
#
# td.column(:sales_stage, :string, :limit => 20, :default => 'new', :null => false)
# # => sales_stage VARCHAR(20) DEFAULT 'new' NOT NULL
#
# td.column(:bill_gates_money, :decimal, :precision => 15, :scale => 2)
# # => bill_gates_money DECIMAL(15,2)
#
# td.column(:sensor_reading, :decimal, :precision => 30, :scale => 20)
# # => sensor_reading DECIMAL(30,20)
#
# # While <tt>:scale</tt> defaults to zero on most databases, it
# # probably wouldn't hurt to include it.
# td.column(:huge_integer, :decimal, :precision => 30)
# # => huge_integer DECIMAL(30)
#
# == Short-hand examples
#
# Instead of calling +column+ directly, you can also work with the short-hand definitions for the default types.
# They use the type as the method name instead of as a parameter and allow for multiple columns to be defined
# in a single statement.
#
# What can be written like this with the regular calls to column:
#
# create_table "products", :force => true do |t|
# t.column "shop_id", :integer
# t.column "creator_id", :integer
# t.column "name", :string, :default => "Untitled"
# t.column "value", :string, :default => "Untitled"
# t.column "created_at", :datetime
# t.column "updated_at", :datetime
# end
#
# Can also be written as follows using the short-hand:
#
# create_table :products do |t|
# t.integer :shop_id, :creator_id
# t.string :name, :value, :default => "Untitled"
# t.timestamps
# end
#
# There's a short-hand method for each of the type values declared at the top. And then there's
# TableDefinition#timestamps that'll add created_at and +updated_at+ as datetimes.
#
# TableDefinition#references will add an appropriately-named _id column, plus a corresponding _type
# column if the <tt>:polymorphic</tt> option is supplied. If <tt>:polymorphic</tt> is a hash of options, these will be
# used when creating the <tt>_type</tt> column. So what can be written like this:
#
# create_table :taggings do |t|
# t.integer :tag_id, :tagger_id, :taggable_id
# t.string :tagger_type
# t.string :taggable_type, :default => 'Photo'
# end
#
# Can also be written as follows using references:
#
# create_table :taggings do |t|
# t.references :tag
# t.references :tagger, :polymorphic => true
# t.references :taggable, :polymorphic => { :default => 'Photo' }
# end
def column(name, type, options = {})
column = self[name] || ColumnDefinition.new(@base, name, type)
if options[:limit]
column.limit = options[:limit]
elsif native[type.to_sym].is_a?(Hash)
column.limit = native[type.to_sym][:limit]
end
column.precision = options[:precision]
column.scale = options[:scale]
column.default = options[:default]
column.null = options[:null]
@columns << column unless @columns.include? column
self
end
%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
EOV
end
# Appends <tt>:datetime</tt> columns <tt>:created_at</tt> and
# <tt>:updated_at</tt> to the table.
def timestamps
column(:created_at, :datetime)
column(:updated_at, :datetime)
end
def references(*args)
options = args.extract_options!
polymorphic = options.delete(:polymorphic)
args.each do |col|
column("#{col}_id", :integer, options)
column("#{col}_type", :string, polymorphic.is_a?(Hash) ? polymorphic : options) unless polymorphic.nil?
end
end
alias :belongs_to :references
# Returns a String whose contents are the column definitions
# concatenated together. This string can then be prepended and appended to
# to generate the final SQL to create the table.
def to_sql
@columns * ', '
end
private
def native
@base.native_database_types
end
end
# Represents a SQL table in an abstract way for updating a table.
# Also see TableDefinition and SchemaStatements#create_table
#
# Available transformations are:
#
# change_table :table do |t|
# t.column
# t.index
# t.timestamps
# t.change
# t.change_default
# t.rename
# t.references
# t.belongs_to
# t.string
# t.text
# t.integer
# t.float
# t.decimal
# t.datetime
# t.timestamp
# t.time
# t.date
# t.binary
# t.boolean
# t.remove
# t.remove_references
# t.remove_belongs_to
# t.remove_index
# t.remove_timestamps
# end
#
class Table
def initialize(table_name, base)
@table_name = table_name
@base = base
end
# Adds a new column to the named table.
# See TableDefinition#column for details of the options you can use.
# ===== Example
# ====== Creating a simple column
# t.column(:name, :string)
def column(column_name, type, options = {})
@base.add_column(@table_name, column_name, type, options)
end
# Adds a new index to the table. +column_name+ can be a single Symbol, or
# an Array of Symbols. See SchemaStatements#add_index
#
# ===== Examples
# ====== Creating a simple index
# t.index(:name)
# ====== Creating a unique index
# t.index([:branch_id, :party_id], :unique => true)
# ====== Creating a named index
# t.index([:branch_id, :party_id], :unique => true, :name => 'by_branch_party')
def index(column_name, options = {})
@base.add_index(@table_name, column_name, options)
end
# Adds timestamps (created_at and updated_at) columns to the table. See SchemaStatements#add_timestamps
# ===== Example
# t.timestamps
def timestamps
@base.add_timestamps(@table_name)
end
# Changes the column's definition according to the new options.
# See TableDefinition#column for details of the options you can use.
# ===== Examples
# t.change(:name, :string, :limit => 80)
# t.change(:description, :text)
def change(column_name, type, options = {})
@base.change_column(@table_name, column_name, type, options)
end
# Sets a new default value for a column. See SchemaStatements#change_column_default
# ===== Examples
# t.change_default(:qualification, 'new')
# t.change_default(:authorized, 1)
def change_default(column_name, default)
@base.change_column_default(@table_name, column_name, default)
end
# Removes the column(s) from the table definition.
# ===== Examples
# t.remove(:qualification)
# t.remove(:qualification, :experience)
def remove(*column_names)
@base.remove_column(@table_name, column_names)
end
# Removes the given index from the table.
#
# ===== Examples
# ====== Remove the suppliers_name_index in the suppliers table
# t.remove_index :name
# ====== Remove the index named accounts_branch_id_index in the accounts table
# t.remove_index :column => :branch_id
# ====== Remove the index named accounts_branch_id_party_id_index in the accounts table
# t.remove_index :column => [:branch_id, :party_id]
# ====== Remove the index named by_branch_party in the accounts table
# t.remove_index :name => :by_branch_party
def remove_index(options = {})
@base.remove_index(@table_name, options)
end
# Removes the timestamp columns (created_at and updated_at) from the table.
# ===== Example
# t.remove_timestamps
def remove_timestamps
@base.remove_timestamps(@table_name)
end
# Renames a column.
# ===== Example
# t.rename(:description, :name)
def rename(column_name, new_column_name)
@base.rename_column(@table_name, column_name, new_column_name)
end
# Adds a reference. Optionally adds a +type+ column.
# <tt>references</tt> and <tt>belongs_to</tt> are acceptable.
# ===== Examples
# t.references(:goat)
# t.references(:goat, :polymorphic => true)
# t.belongs_to(:goat)
def references(*args)
options = args.extract_options!
polymorphic = options.delete(:polymorphic)
args.each do |col|
@base.add_column(@table_name, "#{col}_id", :integer, options)
@base.add_column(@table_name, "#{col}_type", :string, polymorphic.is_a?(Hash) ? polymorphic : options) unless polymorphic.nil?
end
end
alias :belongs_to :references
# Removes a reference. Optionally removes a +type+ column.
# <tt>remove_references</tt> and <tt>remove_belongs_to</tt> are acceptable.
# ===== Examples
# t.remove_references(:goat)
# t.remove_references(:goat, :polymorphic => true)
# t.remove_belongs_to(:goat)
def remove_references(*args)
options = args.extract_options!
polymorphic = options.delete(:polymorphic)
args.each do |col|
@base.remove_column(@table_name, "#{col}_id")
@base.remove_column(@table_name, "#{col}_type") unless polymorphic.nil?
end
end
alias :remove_belongs_to :remove_references
# Adds a column or columns of a specified type
# ===== Examples
# t.string(:goat)
# 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
EOV
end
private
def native
@base.native_database_types
end
end
end
end

View file

@ -1,421 +0,0 @@
module ActiveRecord
module ConnectionAdapters # :nodoc:
module SchemaStatements
# Returns a Hash of mappings from the abstract data types to the native
# database types. See TableDefinition#column for details on the recognized
# abstract data types.
def native_database_types
{}
end
# This is the maximum length a table alias can be
def table_alias_length
255
end
# Truncates a table alias according to the limits of the current adapter.
def table_alias_for(table_name)
table_name[0..table_alias_length-1].gsub(/\./, '_')
end
# def tables(name = nil) end
def table_exists?(table_name)
tables.include?(table_name.to_s)
end
# Returns an array of indexes for the given table.
# def indexes(table_name, name = nil) end
# Returns an array of Column objects for the table specified by +table_name+.
# See the concrete implementation for details on the expected parameter values.
def columns(table_name, name = nil) end
# Creates a new table
# There are two ways to work with +create_table+. You can use the block
# form or the regular form, like this:
#
# === Block form
# # create_table() yields a TableDefinition instance
# create_table(:suppliers) do |t|
# t.column :name, :string, :limit => 60
# # Other fields here
# end
#
# === Regular form
# create_table(:suppliers)
# add_column(:suppliers, :name, :string, {:limit => 60})
#
# The +options+ hash can include the following keys:
# [<tt>:id</tt>]
# Whether to automatically add a primary key column. Defaults to true.
# Join tables for +has_and_belongs_to_many+ should set <tt>:id => false</tt>.
# [<tt>:primary_key</tt>]
# The name of the primary key, if one is to be added automatically.
# Defaults to +id+.
# [<tt>:options</tt>]
# Any extra options you want appended to the table definition.
# [<tt>:temporary</tt>]
# Make a temporary table.
# [<tt>:force</tt>]
# Set to true to drop the table before creating it.
# Defaults to false.
#
# ===== Examples
# ====== Add a backend specific option to the generated SQL (MySQL)
# create_table(:suppliers, :options => 'ENGINE=InnoDB DEFAULT CHARSET=utf8')
# generates:
# CREATE TABLE suppliers (
# id int(11) DEFAULT NULL auto_increment PRIMARY KEY
# ) ENGINE=InnoDB DEFAULT CHARSET=utf8
#
# ====== Rename the primary key column
# create_table(:objects, :primary_key => 'guid') do |t|
# t.column :name, :string, :limit => 80
# end
# generates:
# CREATE TABLE objects (
# guid int(11) DEFAULT NULL auto_increment PRIMARY KEY,
# name varchar(80)
# )
#
# ====== Do not add a primary key column
# create_table(:categories_suppliers, :id => false) do |t|
# t.column :category_id, :integer
# t.column :supplier_id, :integer
# end
# generates:
# CREATE TABLE categories_suppliers (
# category_id int,
# supplier_id int
# )
#
# See also TableDefinition#column for details on how to create columns.
def create_table(table_name, options = {})
table_definition = TableDefinition.new(self)
table_definition.primary_key(options[:primary_key] || Base.get_primary_key(table_name)) unless options[:id] == false
yield table_definition
if options[:force] && table_exists?(table_name)
drop_table(table_name, options)
end
create_sql = "CREATE#{' TEMPORARY' if options[:temporary]} TABLE "
create_sql << "#{quote_table_name(table_name)} ("
create_sql << table_definition.to_sql
create_sql << ") #{options[:options]}"
execute create_sql
end
# A block for changing columns in +table+.
#
# === Example
# # change_table() yields a Table instance
# change_table(:suppliers) do |t|
# t.column :name, :string, :limit => 60
# # Other column alterations here
# end
#
# ===== Examples
# ====== Add a column
# change_table(:suppliers) do |t|
# t.column :name, :string, :limit => 60
# end
#
# ====== Add 2 integer columns
# change_table(:suppliers) do |t|
# t.integer :width, :height, :null => false, :default => 0
# end
#
# ====== Add created_at/updated_at columns
# change_table(:suppliers) do |t|
# t.timestamps
# end
#
# ====== Add a foreign key column
# change_table(:suppliers) do |t|
# t.references :company
# end
#
# Creates a <tt>company_id(integer)</tt> column
#
# ====== Add a polymorphic foreign key column
# change_table(:suppliers) do |t|
# t.belongs_to :company, :polymorphic => true
# end
#
# Creates <tt>company_type(varchar)</tt> and <tt>company_id(integer)</tt> columns
#
# ====== Remove a column
# change_table(:suppliers) do |t|
# t.remove :company
# end
#
# ====== Remove several columns
# change_table(:suppliers) do |t|
# t.remove :company_id
# t.remove :width, :height
# end
#
# ====== Remove an index
# change_table(:suppliers) do |t|
# t.remove_index :company_id
# end
#
# See also Table for details on
# all of the various column transformation
def change_table(table_name)
yield Table.new(table_name, self)
end
# Renames a table.
# ===== Example
# rename_table('octopuses', 'octopi')
def rename_table(table_name, new_name)
raise NotImplementedError, "rename_table is not implemented"
end
# Drops a table from the database.
def drop_table(table_name, options = {})
execute "DROP TABLE #{quote_table_name(table_name)}"
end
# Adds a new column to the named table.
# See TableDefinition#column for details of the options you can use.
def add_column(table_name, column_name, type, options = {})
add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
add_column_options!(add_column_sql, options)
execute(add_column_sql)
end
# Removes the column(s) from the table definition.
# ===== Examples
# remove_column(:suppliers, :qualification)
# remove_columns(:suppliers, :qualification, :experience)
def remove_column(table_name, *column_names)
column_names.flatten.each do |column_name|
execute "ALTER TABLE #{quote_table_name(table_name)} DROP #{quote_column_name(column_name)}"
end
end
alias :remove_columns :remove_column
# Changes the column's definition according to the new options.
# See TableDefinition#column for details of the options you can use.
# ===== Examples
# change_column(:suppliers, :name, :string, :limit => 80)
# change_column(:accounts, :description, :text)
def change_column(table_name, column_name, type, options = {})
raise NotImplementedError, "change_column is not implemented"
end
# Sets a new default value for a column. If you want to set the default
# value to +NULL+, you are out of luck. You need to
# DatabaseStatements#execute the appropriate SQL statement yourself.
# ===== Examples
# change_column_default(:suppliers, :qualification, 'new')
# change_column_default(:accounts, :authorized, 1)
def change_column_default(table_name, column_name, default)
raise NotImplementedError, "change_column_default is not implemented"
end
# Renames a column.
# ===== Example
# rename_column(:suppliers, :description, :name)
def rename_column(table_name, column_name, new_column_name)
raise NotImplementedError, "rename_column is not implemented"
end
# Adds a new index to the table. +column_name+ can be a single Symbol, or
# an Array of Symbols.
#
# The index will be named after the table and the first column name,
# unless you pass <tt>:name</tt> as an option.
#
# When creating an index on multiple columns, the first column is used as a name
# for the index. For example, when you specify an index on two columns
# [<tt>:first</tt>, <tt>:last</tt>], the DBMS creates an index for both columns as well as an
# index for the first column <tt>:first</tt>. Using just the first name for this index
# makes sense, because you will never have to create a singular index with this
# name.
#
# ===== Examples
# ====== Creating a simple index
# add_index(:suppliers, :name)
# generates
# CREATE INDEX suppliers_name_index ON suppliers(name)
# ====== Creating a unique index
# add_index(:accounts, [:branch_id, :party_id], :unique => true)
# generates
# CREATE UNIQUE INDEX accounts_branch_id_party_id_index ON accounts(branch_id, party_id)
# ====== Creating a named index
# add_index(:accounts, [:branch_id, :party_id], :unique => true, :name => 'by_branch_party')
# generates
# CREATE UNIQUE INDEX by_branch_party ON accounts(branch_id, party_id)
def add_index(table_name, column_name, options = {})
column_names = Array(column_name)
index_name = index_name(table_name, :column => column_names)
if Hash === options # legacy support, since this param was a string
index_type = options[:unique] ? "UNIQUE" : ""
index_name = options[:name] || index_name
else
index_type = options
end
quoted_column_names = column_names.map { |e| quote_column_name(e) }.join(", ")
execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} (#{quoted_column_names})"
end
# Remove the given index from the table.
#
# Remove the suppliers_name_index in the suppliers table.
# remove_index :suppliers, :name
# Remove the index named accounts_branch_id_index in the accounts table.
# remove_index :accounts, :column => :branch_id
# Remove the index named accounts_branch_id_party_id_index in the accounts table.
# remove_index :accounts, :column => [:branch_id, :party_id]
# Remove the index named by_branch_party in the accounts table.
# remove_index :accounts, :name => :by_branch_party
def remove_index(table_name, options = {})
execute "DROP INDEX #{quote_column_name(index_name(table_name, options))} ON #{table_name}"
end
def index_name(table_name, options) #:nodoc:
if Hash === options # legacy support
if options[:column]
"index_#{table_name}_on_#{Array(options[:column]) * '_and_'}"
elsif options[:name]
options[:name]
else
raise ArgumentError, "You must specify the index name"
end
else
index_name(table_name, :column => options)
end
end
# Returns a string of <tt>CREATE TABLE</tt> SQL statement(s) for recreating the
# entire structure of the database.
def structure_dump
end
def dump_schema_information #:nodoc:
sm_table = ActiveRecord::Migrator.schema_migrations_table_name
migrated = select_values("SELECT version FROM #{sm_table}")
migrated.map { |v| "INSERT INTO #{sm_table} (version) VALUES ('#{v}');" }.join("\n\n")
end
# Should not be called normally, but this operation is non-destructive.
# The migrations module handles this automatically.
def initialize_schema_migrations_table
sm_table = ActiveRecord::Migrator.schema_migrations_table_name
unless tables.detect { |t| t == sm_table }
create_table(sm_table, :id => false) do |schema_migrations_table|
schema_migrations_table.column :version, :string, :null => false
end
add_index sm_table, :version, :unique => true,
:name => 'unique_schema_migrations'
# Backwards-compatibility: if we find schema_info, assume we've
# migrated up to that point:
si_table = Base.table_name_prefix + 'schema_info' + Base.table_name_suffix
if tables.detect { |t| t == si_table }
old_version = select_value("SELECT version FROM #{quote_table_name(si_table)}").to_i
assume_migrated_upto_version(old_version)
drop_table(si_table)
end
end
end
def assume_migrated_upto_version(version)
sm_table = quote_table_name(ActiveRecord::Migrator.schema_migrations_table_name)
migrated = select_values("SELECT version FROM #{sm_table}").map(&:to_i)
versions = Dir['db/migrate/[0-9]*_*.rb'].map do |filename|
filename.split('/').last.split('_').first.to_i
end
execute "INSERT INTO #{sm_table} (version) VALUES ('#{version}')" unless migrated.include?(version.to_i)
(versions - migrated).select { |v| v < version.to_i }.each do |v|
execute "INSERT INTO #{sm_table} (version) VALUES ('#{v}')"
end
end
def type_to_sql(type, limit = nil, precision = nil, scale = nil) #:nodoc:
if native = native_database_types[type]
column_type_sql = native.is_a?(Hash) ? native[:name] : native
if type == :decimal # ignore limit, use precision and scale
scale ||= native[:scale]
if precision ||= native[:precision]
if scale
column_type_sql << "(#{precision},#{scale})"
else
column_type_sql << "(#{precision})"
end
elsif scale
raise ArgumentError, "Error adding decimal column: precision cannot be empty if scale if specified"
end
elsif limit ||= native.is_a?(Hash) && native[:limit]
column_type_sql << "(#{limit})"
end
column_type_sql
else
type
end
end
def add_column_options!(sql, options) #:nodoc:
sql << " DEFAULT #{quote(options[:default], options[:column])}" if options_include_default?(options)
# must explcitly check for :null to allow change_column to work on migrations
if options.has_key? :null
if options[:null] == false
sql << " NOT NULL"
else
sql << " NULL"
end
end
end
# SELECT DISTINCT clause for a given set of columns and a given ORDER BY clause.
# Both PostgreSQL and Oracle overrides this for custom DISTINCT syntax.
#
# distinct("posts.id", "posts.created_at desc")
def distinct(columns, order_by)
"DISTINCT #{columns}"
end
# ORDER BY clause for the passed order option.
# PostgreSQL overrides this due to its stricter standards compliance.
def add_order_by_for_association_limiting!(sql, options)
sql << " ORDER BY #{options[:order]}"
end
# Adds timestamps (created_at and updated_at) columns to the named table.
# ===== Examples
# add_timestamps(:suppliers)
def add_timestamps(table_name)
add_column table_name, :created_at, :datetime
add_column table_name, :updated_at, :datetime
end
# Removes the timestamp columns (created_at and updated_at) from the table definition.
# ===== Examples
# remove_timestamps(:suppliers)
def remove_timestamps(table_name)
remove_column table_name, :updated_at
remove_column table_name, :created_at
end
protected
def options_include_default?(options)
options.include?(:default) && !(options[:null] == false && options[:default].nil?)
end
end
end
end

View file

@ -1,169 +0,0 @@
require 'benchmark'
require 'date'
require 'bigdecimal'
require 'bigdecimal/util'
require 'active_record/connection_adapters/abstract/schema_definitions'
require 'active_record/connection_adapters/abstract/schema_statements'
require 'active_record/connection_adapters/abstract/database_statements'
require 'active_record/connection_adapters/abstract/quoting'
require 'active_record/connection_adapters/abstract/connection_specification'
require 'active_record/connection_adapters/abstract/query_cache'
module ActiveRecord
module ConnectionAdapters # :nodoc:
# All the concrete database adapters follow the interface laid down in this class.
# You can use this interface directly by borrowing the database connection from the Base with
# Base.connection.
#
# Most of the methods in the adapter are useful during migrations. Most
# notably, SchemaStatements#create_table, SchemaStatements#drop_table,
# SchemaStatements#add_index, SchemaStatements#remove_index,
# SchemaStatements#add_column, SchemaStatements#change_column and
# SchemaStatements#remove_column are very useful.
class AbstractAdapter
include Quoting, DatabaseStatements, SchemaStatements
include QueryCache
@@row_even = true
def initialize(connection, logger = nil) #:nodoc:
@connection, @logger = connection, logger
@runtime = 0
@last_verification = 0
@query_cache_enabled = false
end
# Returns the human-readable name of the adapter. Use mixed case - one
# can always use downcase if needed.
def adapter_name
'Abstract'
end
# Does this adapter support migrations? Backend specific, as the
# abstract adapter always returns +false+.
def supports_migrations?
false
end
# Does this adapter support using DISTINCT within COUNT? This is +true+
# for all adapters except sqlite.
def supports_count_distinct?
true
end
# Should primary key values be selected from their corresponding
# sequence before the insert statement? If true, next_sequence_value
# is called before each insert to set the record's primary key.
# This is false for all adapters but Firebird.
def prefetch_primary_key?(table_name = nil)
false
end
def reset_runtime #:nodoc:
rt, @runtime = @runtime, 0
rt
end
# QUOTING ==================================================
# Override to return the quoted table name. Defaults to column quoting.
def quote_table_name(name)
quote_column_name(name)
end
# REFERENTIAL INTEGRITY ====================================
# Override to turn off referential integrity while executing <tt>&block</tt>.
def disable_referential_integrity(&block)
yield
end
# CONNECTION MANAGEMENT ====================================
# Is this connection active and ready to perform queries?
def active?
@active != false
end
# Close this connection and open a new one in its place.
def reconnect!
@active = true
end
# Close this connection
def disconnect!
@active = false
end
# Returns true if its safe to reload the connection between requests for development mode.
# This is not the case for Ruby/MySQL and it's not necessary for any adapters except SQLite.
def requires_reloading?
false
end
# Lazily verify this connection, calling <tt>active?</tt> only if it hasn't
# been called for +timeout+ seconds.
def verify!(timeout)
now = Time.now.to_i
if (now - @last_verification) > timeout
reconnect! unless active?
@last_verification = now
end
end
# Provides access to the underlying database connection. Useful for
# when you need to call a proprietary method such as postgresql's lo_*
# methods
def raw_connection
@connection
end
def log_info(sql, name, runtime)
if @logger && @logger.debug?
name = "#{name.nil? ? "SQL" : name} (#{sprintf("%f", runtime)})"
@logger.debug format_log_entry(name, sql.squeeze(' '))
end
end
protected
def log(sql, name)
if block_given?
result = nil
seconds = Benchmark.realtime { result = yield }
@runtime += seconds
log_info(sql, name, seconds)
result
else
log_info(sql, name, 0)
nil
end
rescue Exception => e
# Log message and raise exception.
# Set last_verification to 0, so that connection gets verified
# upon reentering the request loop
@last_verification = 0
message = "#{e.class.name}: #{e.message}: #{sql}"
log_info(message, name, 0)
raise ActiveRecord::StatementInvalid, message
end
def format_log_entry(message, dump = nil)
if ActiveRecord::Base.colorize_logging
if @@row_even
@@row_even = false
message_color, dump_color = "4;36;1", "0;1"
else
@@row_even = true
message_color, dump_color = "4;35;1", "0"
end
log_entry = " \e[#{message_color}m#{message}\e[0m "
log_entry << "\e[#{dump_color}m%#{String === dump ? 's' : 'p'}\e[0m" % dump if dump
log_entry
else
"%s %s" % [message, dump]
end
end
end
end
end

View file

@ -1,530 +0,0 @@
require 'active_record/connection_adapters/abstract_adapter'
require 'set'
module MysqlCompat #:nodoc:
# add all_hashes method to standard mysql-c bindings or pure ruby version
def self.define_all_hashes_method!
raise 'Mysql not loaded' unless defined?(::Mysql)
target = defined?(Mysql::Result) ? Mysql::Result : MysqlRes
return if target.instance_methods.include?('all_hashes')
# Ruby driver has a version string and returns null values in each_hash
# 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
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
end_eval
end
unless target.instance_methods.include?('all_hashes') ||
target.instance_methods.include?(:all_hashes)
raise "Failed to defined #{target.name}#all_hashes method. Mysql::VERSION = #{Mysql::VERSION.inspect}"
end
end
end
module ActiveRecord
class Base
def self.require_mysql
# Include the MySQL driver if one hasn't already been loaded
unless defined? Mysql
begin
require_library_or_gem 'mysql'
rescue LoadError => cannot_require_mysql
# Use the bundled Ruby/MySQL driver if no driver is already in place
begin
ActiveRecord::Base.logger.info(
"WARNING: You're using the Ruby-based MySQL library that ships with Rails. This library is not suited for production. " +
"Please install the C-based MySQL library instead (gem install mysql)."
) if ActiveRecord::Base.logger
require 'active_record/vendor/mysql'
rescue LoadError
raise cannot_require_mysql
end
end
end
# Define Mysql::Result.all_hashes
MysqlCompat.define_all_hashes_method!
end
# Establishes a connection to the database that's used by all Active Record objects.
def self.mysql_connection(config) # :nodoc:
config = config.symbolize_keys
host = config[:host]
port = config[:port]
socket = config[:socket]
username = config[:username] ? config[:username].to_s : 'root'
password = config[:password].to_s
if config.has_key?(:database)
database = config[:database]
else
raise ArgumentError, "No database specified. Missing argument: database."
end
require_mysql
mysql = Mysql.init
mysql.ssl_set(config[:sslkey], config[:sslcert], config[:sslca], config[:sslcapath], config[:sslcipher]) if config[:sslkey]
ConnectionAdapters::MysqlAdapter.new(mysql, logger, [host, username, password, database, port, socket], config)
end
end
module ConnectionAdapters
class MysqlColumn < Column #:nodoc:
def extract_default(default)
if type == :binary || type == :text
if default.blank?
nil
else
raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}"
end
elsif missing_default_forged_as_empty_string?(default)
nil
else
super
end
end
private
def simplified_type(field_type)
return :boolean if MysqlAdapter.emulate_booleans && field_type.downcase.index("tinyint(1)")
return :string if field_type =~ /enum/i
super
end
def extract_limit(sql_type)
if sql_type =~ /blob|text/i
case sql_type
when /tiny/i
255
when /medium/i
16777215
when /long/i
2147483647 # mysql only allows 2^31-1, not 2^32-1, somewhat inconsistently with the tiny/medium/normal cases
else
super # we could return 65535 here, but we leave it undecorated by default
end
else
super
end
end
# MySQL misreports NOT NULL column default when none is given.
# We can't detect this for columns which may have a legitimate ''
# default (string) but we can for others (integer, datetime, boolean,
# and the rest).
#
# Test whether the column has default '', is not null, and is not
# a type allowing default ''.
def missing_default_forged_as_empty_string?(default)
type != :string && !null && default == ''
end
end
# The MySQL adapter will work with both Ruby/MySQL, which is a Ruby-based MySQL adapter that comes bundled with Active Record, and with
# the faster C-based MySQL/Ruby adapter (available both as a gem and from http://www.tmtm.org/en/mysql/ruby/).
#
# Options:
#
# * <tt>:host</tt> - Defaults to "localhost".
# * <tt>:port</tt> - Defaults to 3306.
# * <tt>:socket</tt> - Defaults to "/tmp/mysql.sock".
# * <tt>:username</tt> - Defaults to "root"
# * <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>: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
@@emulate_booleans = true
cattr_accessor :emulate_booleans
LOST_CONNECTION_ERROR_MESSAGES = [
"Server shutdown in progress",
"Broken pipe",
"Lost connection to MySQL server during query",
"MySQL server has gone away" ]
QUOTED_TRUE, QUOTED_FALSE = '1', '0'
def initialize(connection, logger, connection_options, config)
super(connection, logger)
@connection_options, @config = connection_options, config
@quoted_column_names, @quoted_table_names = {}, {}
connect
end
def adapter_name #:nodoc:
'MySQL'
end
def supports_migrations? #:nodoc:
true
end
def native_database_types #:nodoc:
{
:primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY",
:string => { :name => "varchar", :limit => 255 },
:text => { :name => "text" },
:integer => { :name => "int"},
:float => { :name => "float" },
:decimal => { :name => "decimal" },
:datetime => { :name => "datetime" },
:timestamp => { :name => "datetime" },
:time => { :name => "time" },
:date => { :name => "date" },
:binary => { :name => "blob" },
:boolean => { :name => "tinyint", :limit => 1 }
}
end
# QUOTING ==================================================
def quote(value, column = nil)
if value.kind_of?(String) && column && column.type == :binary && column.class.respond_to?(:string_to_binary)
s = column.class.string_to_binary(value).unpack("H*")[0]
"x'#{s}'"
elsif value.kind_of?(BigDecimal)
"'#{value.to_s("F")}'"
else
super
end
end
def quote_column_name(name) #:nodoc:
@quoted_column_names[name] ||= "`#{name}`"
end
def quote_table_name(name) #:nodoc:
@quoted_table_names[name] ||= quote_column_name(name).gsub('.', '`.`')
end
def quote_string(string) #:nodoc:
@connection.quote(string)
end
def quoted_true
QUOTED_TRUE
end
def quoted_false
QUOTED_FALSE
end
# REFERENTIAL INTEGRITY ====================================
def disable_referential_integrity(&block) #:nodoc:
old = select_value("SELECT @@FOREIGN_KEY_CHECKS")
begin
update("SET FOREIGN_KEY_CHECKS = 0")
yield
ensure
update("SET FOREIGN_KEY_CHECKS = #{old}")
end
end
# CONNECTION MANAGEMENT ====================================
def active?
if @connection.respond_to?(:stat)
@connection.stat
else
@connection.query 'select 1'
end
# mysql-ruby doesn't raise an exception when stat fails.
if @connection.respond_to?(:errno)
@connection.errno.zero?
else
true
end
rescue Mysql::Error
false
end
def reconnect!
disconnect!
connect
end
def disconnect!
@connection.close rescue nil
end
# DATABASE STATEMENTS ======================================
def select_rows(sql, name = nil)
@connection.query_with_result = true
result = execute(sql, name)
rows = []
result.each { |row| rows << row }
result.free
rows
end
def execute(sql, name = nil) #:nodoc:
log(sql, name) { @connection.query(sql) }
rescue ActiveRecord::StatementInvalid => exception
if exception.message.split(":").first =~ /Packets out of order/
raise ActiveRecord::StatementInvalid, "'Packets out of order' error was received from the database. Please update your mysql bindings (gem install mysql) and read http://dev.mysql.com/doc/mysql/en/password-hashing.html for more information. If you're on Windows, use the Instant Rails installer to get the updated mysql bindings."
else
raise
end
end
def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc:
super sql, name
id_value || @connection.insert_id
end
def update_sql(sql, name = nil) #:nodoc:
super
@connection.affected_rows
end
def begin_db_transaction #:nodoc:
execute "BEGIN"
rescue Exception
# Transactions aren't supported
end
def commit_db_transaction #:nodoc:
execute "COMMIT"
rescue Exception
# Transactions aren't supported
end
def rollback_db_transaction #:nodoc:
execute "ROLLBACK"
rescue Exception
# Transactions aren't supported
end
def add_limit_offset!(sql, options) #:nodoc:
if limit = options[:limit]
unless offset = options[:offset]
sql << " LIMIT #{limit}"
else
sql << " LIMIT #{offset}, #{limit}"
end
end
end
# SCHEMA STATEMENTS ========================================
def structure_dump #:nodoc:
if supports_views?
sql = "SHOW FULL TABLES WHERE Table_type = 'BASE TABLE'"
else
sql = "SHOW TABLES"
end
select_all(sql).inject("") do |structure, table|
table.delete('Table_type')
structure += select_one("SHOW CREATE TABLE #{quote_table_name(table.to_a.first.last)}")["Create Table"] + ";\n\n"
end
end
def recreate_database(name) #:nodoc:
drop_database(name)
create_database(name)
end
# Create a new MySQL database with optional <tt>:charset</tt> and <tt>:collation</tt>.
# Charset defaults to utf8.
#
# Example:
# create_database 'charset_test', :charset => 'latin1', :collation => 'latin1_bin'
# create_database 'matt_development'
# create_database 'matt_development', :charset => :big5
def create_database(name, options = {})
if options[:collation]
execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}` COLLATE `#{options[:collation]}`"
else
execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}`"
end
end
def drop_database(name) #:nodoc:
execute "DROP DATABASE IF EXISTS `#{name}`"
end
def current_database
select_value 'SELECT DATABASE() as db'
end
# Returns the database character set.
def charset
show_variable 'character_set_database'
end
# Returns the database collation strategy.
def collation
show_variable 'collation_database'
end
def tables(name = nil) #:nodoc:
tables = []
execute("SHOW TABLES", name).each { |field| tables << field[0] }
tables
end
def drop_table(table_name, options = {})
super(table_name, options)
end
def indexes(table_name, name = nil)#:nodoc:
indexes = []
current_index = nil
execute("SHOW KEYS FROM #{quote_table_name(table_name)}", name).each do |row|
if current_index != row[2]
next if row[2] == "PRIMARY" # skip the primary key
current_index = row[2]
indexes << IndexDefinition.new(row[0], row[2], row[1] == "0", [])
end
indexes.last.columns << row[4]
end
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") }
columns
end
def create_table(table_name, options = {}) #:nodoc:
super(table_name, options.reverse_merge(:options => "ENGINE=InnoDB"))
end
def rename_table(table_name, new_name)
execute "RENAME TABLE #{quote_table_name(table_name)} TO #{quote_table_name(new_name)}"
end
def change_column_default(table_name, column_name, default) #:nodoc:
current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'")["Type"]
execute("ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(column_name)} #{current_type} DEFAULT #{quote(default)}")
end
def change_column(table_name, column_name, type, options = {}) #:nodoc:
unless options_include_default?(options)
if column = columns(table_name).find { |c| c.name == column_name.to_s }
options[:default] = column.default
else
raise "No such column: #{table_name}.#{column_name}"
end
end
change_column_sql = "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
add_column_options!(change_column_sql, options)
execute(change_column_sql)
end
def rename_column(table_name, column_name, new_column_name) #:nodoc:
current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'")["Type"]
execute "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(new_column_name)} #{current_type}"
end
# Maps logical Rails types to MySQL-specific data types.
def type_to_sql(type, limit = nil, precision = nil, scale = nil)
return super unless type.to_s == 'integer'
case limit
when 0..3
"smallint(#{limit})"
when 4..8
"int(#{limit})"
when 9..20
"bigint(#{limit})"
else
'int(11)'
end
end
# SHOW VARIABLES LIKE 'name'
def show_variable(name)
variables = select_all("SHOW VARIABLES LIKE '#{name}'")
variables.first['Value'] unless variables.empty?
end
# 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|
keys << h["Field"]if h["Key"] == "PRI"
end
keys.length == 1 ? [keys.first, nil] : nil
end
private
def connect
encoding = @config[:encoding]
if encoding
@connection.options(Mysql::SET_CHARSET_NAME, encoding) rescue nil
end
@connection.ssl_set(@config[:sslkey], @config[:sslcert], @config[:sslca], @config[:sslcapath], @config[:sslcipher]) if @config[:sslkey]
@connection.real_connect(*@connection_options)
execute("SET NAMES '#{encoding}'") if encoding
# By default, MySQL 'where id is null' selects the last inserted id.
# Turn this off. http://dev.rubyonrails.org/ticket/6778
execute("SET SQL_AUTO_IS_NULL=0")
end
def select(sql, name = nil)
@connection.query_with_result = true
result = execute(sql, name)
rows = result.all_hashes
result.free
rows
end
def supports_views?
version[0] >= 5
end
def version
@version ||= @connection.server_info.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i }
end
end
end
end

View file

@ -1,948 +0,0 @@
require 'active_record/connection_adapters/abstract_adapter'
begin
require_library_or_gem 'pg'
rescue LoadError => e
begin
require_library_or_gem 'postgres'
class PGresult
alias_method :nfields, :num_fields unless self.method_defined?(:nfields)
alias_method :ntuples, :num_tuples unless self.method_defined?(:ntuples)
alias_method :ftype, :type unless self.method_defined?(:ftype)
alias_method :cmd_tuples, :cmdtuples unless self.method_defined?(:cmd_tuples)
end
rescue LoadError
raise e
end
end
module ActiveRecord
class Base
# Establishes a connection to the database that's used by all Active Record objects
def self.postgresql_connection(config) # :nodoc:
config = config.symbolize_keys
host = config[:host]
port = config[:port] || 5432
username = config[:username].to_s
password = config[:password].to_s
if config.has_key?(:database)
database = config[:database]
else
raise ArgumentError, "No database specified. Missing argument: database."
end
# The postgres drivers don't allow the creation of an unconnected PGconn object,
# so just pass a nil connection object for the time being.
ConnectionAdapters::PostgreSQLAdapter.new(nil, logger, [host, port, nil, nil, database, username, password], config)
end
end
module ConnectionAdapters
# PostgreSQL-specific extensions to column definitions in a table.
class PostgreSQLColumn < Column #:nodoc:
# Instantiates a new PostgreSQL column definition in a table.
def initialize(name, default, sql_type = nil, null = true)
super(name, self.class.extract_value_from_default(default), sql_type, null)
end
private
# Extracts the scale from PostgreSQL-specific data types.
def extract_scale(sql_type)
# Money type has a fixed scale of 2.
sql_type =~ /^money/ ? 2 : super
end
# Extracts the precision from PostgreSQL-specific data types.
def extract_precision(sql_type)
# Actual code is defined dynamically in PostgreSQLAdapter.connect
# depending on the server specifics
super
end
# Escapes binary strings for bytea input to the database.
def self.string_to_binary(value)
if PGconn.respond_to?(:escape_bytea)
self.class.module_eval do
define_method(:string_to_binary) do |value|
PGconn.escape_bytea(value) if value
end
end
else
self.class.module_eval do
define_method(:string_to_binary) do |value|
if value
result = ''
value.each_byte { |c| result << sprintf('\\\\%03o', c) }
result
end
end
end
end
self.class.string_to_binary(value)
end
# Unescapes bytea output from a database to the binary string it represents.
def self.binary_to_string(value)
# In each case, check if the value actually is escaped PostgreSQL bytea output
# or an unescaped Active Record attribute that was just written.
if PGconn.respond_to?(:unescape_bytea)
self.class.module_eval do
define_method(:binary_to_string) do |value|
if value =~ /\\\d{3}/
PGconn.unescape_bytea(value)
else
value
end
end
end
else
self.class.module_eval do
define_method(:binary_to_string) do |value|
if value =~ /\\\d{3}/
result = ''
i, max = 0, value.size
while i < max
char = value[i]
if char == ?\\
if value[i+1] == ?\\
char = ?\\
i += 1
else
char = value[i+1..i+3].oct
i += 3
end
end
result << char
i += 1
end
result
else
value
end
end
end
end
self.class.binary_to_string(value)
end
# Maps PostgreSQL-specific data types to logical Rails types.
def simplified_type(field_type)
case field_type
# Numeric and monetary types
when /^(?:real|double precision)$/
:float
# Monetary types
when /^money$/
:decimal
# Character types
when /^(?:character varying|bpchar)(?:\(\d+\))?$/
:string
# Binary data types
when /^bytea$/
:binary
# Date/time types
when /^timestamp with(?:out)? time zone$/
:datetime
when /^interval$/
:string
# Geometric types
when /^(?:point|line|lseg|box|"?path"?|polygon|circle)$/
:string
# Network address types
when /^(?:cidr|inet|macaddr)$/
:string
# Bit strings
when /^bit(?: varying)?(?:\(\d+\))?$/
:string
# XML type
when /^xml$/
:string
# Arrays
when /^\D+\[\]$/
:string
# Object identifier types
when /^oid$/
:integer
# Pass through all types that are not specific to PostgreSQL.
else
super
end
end
# Extracts the value from a PostgreSQL column default definition.
def self.extract_value_from_default(default)
case default
# Numeric types
when /\A-?\d+(\.\d*)?\z/
default
# Character types
when /\A'(.*)'::(?:character varying|bpchar|text)\z/m
$1
# Character types (8.1 formatting)
when /\AE'(.*)'::(?:character varying|bpchar|text)\z/m
$1.gsub(/\\(\d\d\d)/) { $1.oct.chr }
# Binary data types
when /\A'(.*)'::bytea\z/m
$1
# Date/time types
when /\A'(.+)'::(?:time(?:stamp)? with(?:out)? time zone|date)\z/
$1
when /\A'(.*)'::interval\z/
$1
# Boolean type
when 'true'
true
when 'false'
false
# Geometric types
when /\A'(.*)'::(?:point|line|lseg|box|"?path"?|polygon|circle)\z/
$1
# Network address types
when /\A'(.*)'::(?:cidr|inet|macaddr)\z/
$1
# Bit string types
when /\AB'(.*)'::"?bit(?: varying)?"?\z/
$1
# XML type
when /\A'(.*)'::xml\z/m
$1
# Arrays
when /\A'(.*)'::"?\D+"?\[\]\z/
$1
# Object identifier types
when /\A-?\d+\z/
$1
else
# Anything else is blank, some user type, or some function
# and we can't know the value of that, so return nil.
nil
end
end
end
end
module ConnectionAdapters
# The PostgreSQL adapter works both with the native C (http://ruby.scripting.ca/postgres/) and the pure
# Ruby (available both as gem and from http://rubyforge.org/frs/?group_id=234&release_id=1944) drivers.
#
# Options:
#
# * <tt>:host</tt> - Defaults to "localhost".
# * <tt>:port</tt> - Defaults to 5432.
# * <tt>:username</tt> - Defaults to nothing.
# * <tt>:password</tt> - Defaults to nothing.
# * <tt>:database</tt> - The name of the database. No default, must be provided.
# * <tt>:schema_search_path</tt> - An optional schema search path for the connection given as a string of comma-separated schema names. This is backward-compatible with the <tt>:schema_order</tt> option.
# * <tt>:encoding</tt> - An optional client encoding that is used in a <tt>SET client_encoding TO <encoding></tt> call on the connection.
# * <tt>:min_messages</tt> - An optional client min messages that is used in a <tt>SET client_min_messages TO <min_messages></tt> call on the connection.
# * <tt>:allow_concurrency</tt> - If true, use async query methods so Ruby threads don't deadlock; otherwise, use blocking query methods.
class PostgreSQLAdapter < AbstractAdapter
# Returns 'PostgreSQL' as adapter name for identification purposes.
def adapter_name
'PostgreSQL'
end
# Initializes and connects a PostgreSQL adapter.
def initialize(connection, logger, connection_parameters, config)
super(connection, logger)
@connection_parameters, @config = connection_parameters, config
connect
end
# Is this connection alive and ready for queries?
def active?
if @connection.respond_to?(:status)
@connection.status == PGconn::CONNECTION_OK
else
# We're asking the driver, not ActiveRecord, so use @connection.query instead of #query
@connection.query 'SELECT 1'
true
end
# postgres-pr raises a NoMethodError when querying if no connection is available.
rescue PGError, NoMethodError
false
end
# Close then reopen the connection.
def reconnect!
if @connection.respond_to?(:reset)
@connection.reset
configure_connection
else
disconnect!
connect
end
end
# Close the connection.
def disconnect!
@connection.close rescue nil
end
def native_database_types #:nodoc:
{
:primary_key => "serial primary key",
:string => { :name => "character varying", :limit => 255 },
:text => { :name => "text" },
:integer => { :name => "integer" },
:float => { :name => "float" },
:decimal => { :name => "decimal" },
:datetime => { :name => "timestamp" },
:timestamp => { :name => "timestamp" },
:time => { :name => "time" },
:date => { :name => "date" },
:binary => { :name => "bytea" },
:boolean => { :name => "boolean" }
}
end
# Does PostgreSQL support migrations?
def supports_migrations?
true
end
# Does PostgreSQL support standard conforming strings?
def supports_standard_conforming_strings?
# Temporarily set the client message level above error to prevent unintentional
# error messages in the logs when working on a PostgreSQL database server that
# does not support standard conforming strings.
client_min_messages_old = client_min_messages
self.client_min_messages = 'panic'
# postgres-pr does not raise an exception when client_min_messages is set higher
# than error and "SHOW standard_conforming_strings" fails, but returns an empty
# PGresult instead.
has_support = query('SHOW standard_conforming_strings')[0][0] rescue false
self.client_min_messages = client_min_messages_old
has_support
end
# Returns the configured supported identifier length supported by PostgreSQL,
# or report the default of 63 on PostgreSQL 7.x.
def table_alias_length
@table_alias_length ||= (postgresql_version >= 80000 ? query('SHOW max_identifier_length')[0][0].to_i : 63)
end
# QUOTING ==================================================
# Quotes PostgreSQL-specific data types for SQL input.
def quote(value, column = nil) #:nodoc:
if value.kind_of?(String) && column && column.type == :binary
"#{quoted_string_prefix}'#{column.class.string_to_binary(value)}'"
elsif value.kind_of?(String) && column && column.sql_type =~ /^xml$/
"xml '#{quote_string(value)}'"
elsif value.kind_of?(Numeric) && column && column.sql_type =~ /^money$/
# Not truly string input, so doesn't require (or allow) escape string syntax.
"'#{value.to_s}'"
elsif value.kind_of?(String) && column && column.sql_type =~ /^bit/
case value
when /^[01]*$/
"B'#{value}'" # Bit-string notation
when /^[0-9A-F]*$/i
"X'#{value}'" # Hexadecimal notation
end
else
super
end
end
# Quotes strings for use in SQL input in the postgres driver for better performance.
def quote_string(s) #:nodoc:
if PGconn.respond_to?(:escape)
self.class.instance_eval do
define_method(:quote_string) do |s|
PGconn.escape(s)
end
end
else
# There are some incorrectly compiled postgres drivers out there
# that don't define PGconn.escape.
self.class.instance_eval do
undef_method(:quote_string)
end
end
quote_string(s)
end
# Quotes column names for use in SQL queries.
def quote_column_name(name) #:nodoc:
%("#{name}")
end
# Quote date/time values for use in SQL input. Includes microseconds
# if the value is a Time responding to usec.
def quoted_date(value) #:nodoc:
if value.acts_like?(:time) && value.respond_to?(:usec)
"#{super}.#{sprintf("%06d", value.usec)}"
else
super
end
end
# REFERENTIAL INTEGRITY ====================================
def supports_disable_referential_integrity?() #:nodoc:
version = query("SHOW server_version")[0][0].split('.')
(version[0].to_i >= 8 && version[1].to_i >= 1) ? true : false
rescue
return false
end
def disable_referential_integrity(&block) #:nodoc:
if supports_disable_referential_integrity?() then
execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER ALL" }.join(";"))
end
yield
ensure
if supports_disable_referential_integrity?() then
execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} ENABLE TRIGGER ALL" }.join(";"))
end
end
# DATABASE STATEMENTS ======================================
# Executes a SELECT query and returns an array of rows. Each row is an
# array of field values.
def select_rows(sql, name = nil)
select_raw(sql, name).last
end
# Executes an INSERT query and returns the new record's ID
def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
table = sql.split(" ", 4)[2].gsub('"', '')
super || pk && last_insert_id(table, sequence_name || default_sequence_name(table, pk))
end
# create a 2D array representing the result set
def result_as_array(res) #:nodoc:
ary = []
for i in 0...res.ntuples do
ary << []
for j in 0...res.nfields do
ary[i] << res.getvalue(i,j)
end
end
return ary
end
# Queries the database and returns the results in an Array-like object
def query(sql, name = nil) #:nodoc:
log(sql, name) do
if @async
res = @connection.async_exec(sql)
else
res = @connection.exec(sql)
end
return result_as_array(res)
end
end
# Executes an SQL statement, returning a PGresult object on success
# or raising a PGError exception otherwise.
def execute(sql, name = nil)
log(sql, name) do
if @async
@connection.async_exec(sql)
else
@connection.exec(sql)
end
end
end
# Executes an UPDATE query and returns the number of affected tuples.
def update_sql(sql, name = nil)
super.cmd_tuples
end
# Begins a transaction.
def begin_db_transaction
execute "BEGIN"
end
# Commits a transaction.
def commit_db_transaction
execute "COMMIT"
end
# Aborts a transaction.
def rollback_db_transaction
execute "ROLLBACK"
end
# SCHEMA STATEMENTS ========================================
def recreate_database(name) #:nodoc:
drop_database(name)
create_database(name)
end
# Create a new PostgreSQL database. Options include <tt>:owner</tt>, <tt>:template</tt>,
# <tt>:encoding</tt>, <tt>:tablespace</tt>, and <tt>:connection_limit</tt> (note that MySQL uses
# <tt>:charset</tt> while PostgreSQL uses <tt>:encoding</tt>).
#
# Example:
# create_database config[:database], config
# create_database 'foo_development', :encoding => 'unicode'
def create_database(name, options = {})
options = options.reverse_merge(:encoding => "utf8")
option_string = options.symbolize_keys.sum do |key, value|
case key
when :owner
" OWNER = '#{value}'"
when :template
" TEMPLATE = #{value}"
when :encoding
" ENCODING = '#{value}'"
when :tablespace
" TABLESPACE = #{value}"
when :connection_limit
" CONNECTION LIMIT = #{value}"
else
""
end
end
execute "CREATE DATABASE #{name}#{option_string}"
end
# Drops a PostgreSQL database
#
# Example:
# drop_database 'matt_development'
def drop_database(name) #:nodoc:
execute "DROP DATABASE IF EXISTS #{name}"
end
# Returns the list of all tables in the schema search path or a specified schema.
def tables(name = nil)
schemas = schema_search_path.split(/,/).map { |p| quote(p) }.join(',')
query(<<-SQL, name).map { |row| row[0] }
SELECT tablename
FROM pg_tables
WHERE schemaname IN (#{schemas})
SQL
end
# Returns the list of all indexes for a table.
def indexes(table_name, name = nil)
schemas = schema_search_path.split(/,/).map { |p| quote(p) }.join(',')
result = query(<<-SQL, name)
SELECT distinct i.relname, d.indisunique, a.attname
FROM pg_class t, pg_class i, pg_index d, pg_attribute a
WHERE i.relkind = 'i'
AND d.indexrelid = i.oid
AND d.indisprimary = 'f'
AND t.oid = d.indrelid
AND t.relname = '#{table_name}'
AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname IN (#{schemas}) )
AND a.attrelid = t.oid
AND ( d.indkey[0]=a.attnum OR d.indkey[1]=a.attnum
OR d.indkey[2]=a.attnum OR d.indkey[3]=a.attnum
OR d.indkey[4]=a.attnum OR d.indkey[5]=a.attnum
OR d.indkey[6]=a.attnum OR d.indkey[7]=a.attnum
OR d.indkey[8]=a.attnum OR d.indkey[9]=a.attnum )
ORDER BY i.relname
SQL
current_index = nil
indexes = []
result.each do |row|
if current_index != row[0]
indexes << IndexDefinition.new(table_name, row[0], row[1] == "t", [])
current_index = row[0]
end
indexes.last.columns << row[2]
end
indexes
end
# Returns the list of all column definitions for a table.
def columns(table_name, name = nil)
# Limit, precision, and scale are all handled by the superclass.
column_definitions(table_name).collect do |name, type, default, notnull|
PostgreSQLColumn.new(name, default, type, notnull == 'f')
end
end
# Sets the schema search path to a string of comma-separated schema names.
# Names beginning with $ have to be quoted (e.g. $user => '$user').
# See: http://www.postgresql.org/docs/current/static/ddl-schemas.html
#
# This should be not be called manually but set in database.yml.
def schema_search_path=(schema_csv)
if schema_csv
execute "SET search_path TO #{schema_csv}"
@schema_search_path = schema_csv
end
end
# Returns the active schema search path.
def schema_search_path
@schema_search_path ||= query('SHOW search_path')[0][0]
end
# Returns the current client message level.
def client_min_messages
query('SHOW client_min_messages')[0][0]
end
# Set the client message level.
def client_min_messages=(level)
execute("SET client_min_messages TO '#{level}'")
end
# Returns the sequence name for a table's primary key or some other specified key.
def default_sequence_name(table_name, pk = nil) #:nodoc:
default_pk, default_seq = pk_and_sequence_for(table_name)
default_seq || "#{table_name}_#{pk || default_pk || 'id'}_seq"
end
# Resets the sequence of a table's primary key to the maximum value.
def reset_pk_sequence!(table, pk = nil, sequence = nil) #:nodoc:
unless pk and sequence
default_pk, default_sequence = pk_and_sequence_for(table)
pk ||= default_pk
sequence ||= default_sequence
end
if pk
if sequence
quoted_sequence = quote_column_name(sequence)
select_value <<-end_sql, 'Reset sequence'
SELECT setval('#{quoted_sequence}', (SELECT COALESCE(MAX(#{quote_column_name pk})+(SELECT increment_by FROM #{quoted_sequence}), (SELECT min_value FROM #{quoted_sequence})) FROM #{quote_table_name(table)}), false)
end_sql
else
@logger.warn "#{table} has primary key #{pk} with no default sequence" if @logger
end
end
end
# Returns a table's primary key and belonging sequence.
def pk_and_sequence_for(table) #:nodoc:
# First try looking for a sequence with a dependency on the
# given table's primary key.
result = query(<<-end_sql, 'PK and serial sequence')[0]
SELECT attr.attname, seq.relname
FROM pg_class seq,
pg_attribute attr,
pg_depend dep,
pg_namespace name,
pg_constraint cons
WHERE seq.oid = dep.objid
AND seq.relkind = 'S'
AND attr.attrelid = dep.refobjid
AND attr.attnum = dep.refobjsubid
AND attr.attrelid = cons.conrelid
AND attr.attnum = cons.conkey[1]
AND cons.contype = 'p'
AND dep.refobjid = '#{table}'::regclass
end_sql
if result.nil? or result.empty?
# If that fails, try parsing the primary key's default value.
# Support the 7.x and 8.0 nextval('foo'::text) as well as
# the 8.1+ nextval('foo'::regclass).
result = query(<<-end_sql, 'PK and custom sequence')[0]
SELECT attr.attname,
CASE
WHEN split_part(def.adsrc, '''', 2) ~ '.' THEN
substr(split_part(def.adsrc, '''', 2),
strpos(split_part(def.adsrc, '''', 2), '.')+1)
ELSE split_part(def.adsrc, '''', 2)
END
FROM pg_class t
JOIN pg_attribute attr ON (t.oid = attrelid)
JOIN pg_attrdef def ON (adrelid = attrelid AND adnum = attnum)
JOIN pg_constraint cons ON (conrelid = adrelid AND adnum = conkey[1])
WHERE t.oid = '#{table}'::regclass
AND cons.contype = 'p'
AND def.adsrc ~* 'nextval'
end_sql
end
# [primary_key, sequence]
[result.first, result.last]
rescue
nil
end
# Renames a table.
def rename_table(name, new_name)
execute "ALTER TABLE #{name} RENAME TO #{new_name}"
end
# Adds a new column to the named table.
# See TableDefinition#column for details of the options you can use.
def add_column(table_name, column_name, type, options = {})
default = options[:default]
notnull = options[:null] == false
# Add the column.
execute("ALTER TABLE #{quote_table_name(table_name)} ADD COLUMN #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}")
change_column_default(table_name, column_name, default) if options_include_default?(options)
change_column_null(table_name, column_name, false, default) if notnull
end
# Changes the column of a table.
def change_column(table_name, column_name, type, options = {})
quoted_table_name = quote_table_name(table_name)
begin
execute "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
rescue ActiveRecord::StatementInvalid
# This is PostgreSQL 7.x, so we have to use a more arcane way of doing it.
begin
begin_db_transaction
tmp_column_name = "#{column_name}_ar_tmp"
add_column(table_name, tmp_column_name, type, options)
execute "UPDATE #{quoted_table_name} SET #{quote_column_name(tmp_column_name)} = CAST(#{quote_column_name(column_name)} AS #{type_to_sql(type, options[:limit], options[:precision], options[:scale])})"
remove_column(table_name, column_name)
rename_column(table_name, tmp_column_name, column_name)
commit_db_transaction
rescue
rollback_db_transaction
end
end
change_column_default(table_name, column_name, options[:default]) if options_include_default?(options)
change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null)
end
# Changes the default value of a table column.
def change_column_default(table_name, column_name, default)
execute "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} SET DEFAULT #{quote(default)}"
end
def change_column_null(table_name, column_name, null, default = nil)
unless null || default.nil?
execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
end
execute("ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL")
end
# Renames a column in a table.
def rename_column(table_name, column_name, new_column_name)
execute "ALTER TABLE #{quote_table_name(table_name)} RENAME COLUMN #{quote_column_name(column_name)} TO #{quote_column_name(new_column_name)}"
end
# Drops an index from a table.
def remove_index(table_name, options = {})
execute "DROP INDEX #{index_name(table_name, options)}"
end
# Maps logical Rails types to PostgreSQL-specific data types.
def type_to_sql(type, limit = nil, precision = nil, scale = nil)
return super unless type.to_s == 'integer'
if limit.nil? || limit == 4
'integer'
elsif limit < 4
'smallint'
else
'bigint'
end
end
# Returns a SELECT DISTINCT clause for a given set of columns and a given ORDER BY clause.
#
# PostgreSQL requires the ORDER BY columns in the select list for distinct queries, and
# requires that the ORDER BY include the distinct column.
#
# distinct("posts.id", "posts.created_at desc")
def distinct(columns, order_by) #:nodoc:
return "DISTINCT #{columns}" if order_by.blank?
# Construct a clean list of column names from the ORDER BY clause, removing
# any ASC/DESC modifiers
order_columns = order_by.split(',').collect { |s| s.split.first }
order_columns.delete_if &:blank?
order_columns = order_columns.zip((0...order_columns.size).to_a).map { |s,i| "#{s} AS alias_#{i}" }
# Return a DISTINCT ON() clause that's distinct on the columns we want but includes
# all the required columns for the ORDER BY to work properly.
sql = "DISTINCT ON (#{columns}) #{columns}, "
sql << order_columns * ', '
end
# Returns an ORDER BY clause for the passed order option.
#
# PostgreSQL does not allow arbitrary ordering when using DISTINCT ON, so we work around this
# by wrapping the +sql+ string as a sub-select and ordering in that query.
def add_order_by_for_association_limiting!(sql, options) #:nodoc:
return sql if options[:order].blank?
order = options[:order].split(',').collect { |s| s.strip }.reject(&:blank?)
order.map! { |s| 'DESC' if s =~ /\bdesc$/i }
order = order.zip((0...order.size).to_a).map { |s,i| "id_list.alias_#{i} #{s}" }.join(', ')
sql.replace "SELECT * FROM (#{sql}) AS id_list ORDER BY #{order}"
end
protected
# Returns the version of the connected PostgreSQL version.
def postgresql_version
@postgresql_version ||=
if @connection.respond_to?(:server_version)
@connection.server_version
else
# Mimic PGconn.server_version behavior
begin
query('SELECT version()')[0][0] =~ /PostgreSQL (\d+)\.(\d+)\.(\d+)/
($1.to_i * 10000) + ($2.to_i * 100) + $3.to_i
rescue
0
end
end
end
private
# The internal PostgreSQL identifer of the money data type.
MONEY_COLUMN_TYPE_OID = 790 #:nodoc:
# Connects to a PostgreSQL server and sets up the adapter depending on the
# connected server's characteristics.
def connect
@connection = PGconn.connect(*@connection_parameters)
PGconn.translate_results = false if PGconn.respond_to?(:translate_results=)
# Ignore async_exec and async_query when using postgres-pr.
@async = @config[:allow_concurrency] && @connection.respond_to?(:async_exec)
# Use escape string syntax if available. We cannot do this lazily when encountering
# the first string, because that could then break any transactions in progress.
# See: http://www.postgresql.org/docs/current/static/runtime-config-compatible.html
# If PostgreSQL doesn't know the standard_conforming_strings parameter then it doesn't
# support escape string syntax. Don't override the inherited quoted_string_prefix.
if supports_standard_conforming_strings?
self.class.instance_eval do
define_method(:quoted_string_prefix) { 'E' }
end
end
# Money type has a fixed precision of 10 in PostgreSQL 8.2 and below, and as of
# PostgreSQL 8.3 it has a fixed precision of 19. PostgreSQLColumn.extract_precision
# 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
end_eval
configure_connection
end
# Configures the encoding, verbosity, and schema search path of the connection.
# This is called by #connect and should not be called manually.
def configure_connection
if @config[:encoding]
if @connection.respond_to?(:set_client_encoding)
@connection.set_client_encoding(@config[:encoding])
else
execute("SET client_encoding TO '#{@config[:encoding]}'")
end
end
self.client_min_messages = @config[:min_messages] if @config[:min_messages]
self.schema_search_path = @config[:schema_search_path] || @config[:schema_order]
end
# Returns the current ID of a table's sequence.
def last_insert_id(table, sequence_name) #:nodoc:
Integer(select_value("SELECT currval('#{sequence_name}')"))
end
# Executes a SELECT query and returns the results, performing any data type
# conversions that are required to be performed here instead of in PostgreSQLColumn.
def select(sql, name = nil)
fields, rows = select_raw(sql, name)
result = []
for row in rows
row_hash = {}
fields.each_with_index do |f, i|
row_hash[f] = row[i]
end
result << row_hash
end
result
end
def select_raw(sql, name = nil)
res = execute(sql, name)
results = result_as_array(res)
fields = []
rows = []
if res.ntuples > 0
fields = res.fields
results.each do |row|
hashed_row = {}
row.each_index do |cell_index|
# If this is a money type column and there are any currency symbols,
# then strip them off. Indeed it would be prettier to do this in
# PostgreSQLColumn.string_to_decimal but would break form input
# fields that call value_before_type_cast.
if res.ftype(cell_index) == MONEY_COLUMN_TYPE_OID
# Because money output is formatted according to the locale, there are two
# cases to consider (note the decimal separators):
# (1) $12,345,678.12
# (2) $12.345.678,12
case column = row[cell_index]
when /^-?\D+[\d,]+\.\d{2}$/ # (1)
row[cell_index] = column.gsub(/[^-\d\.]/, '')
when /^-?\D+[\d\.]+,\d{2}$/ # (2)
row[cell_index] = column.gsub(/[^-\d,]/, '').sub(/,/, '.')
end
end
hashed_row[fields[cell_index]] = column
end
rows << row
end
end
res.clear
return fields, rows
end
# Returns the list of a table's column names, data types, and default values.
#
# The underlying query is roughly:
# SELECT column.name, column.type, default.value
# FROM column LEFT JOIN default
# ON column.table_id = default.table_id
# AND column.num = default.column_num
# WHERE column.table_id = get_table_id('table_name')
# AND column.num > 0
# AND NOT column.is_dropped
# ORDER BY column.num
#
# If the table name is not prefixed with a schema, the database will
# take the first match from the schema search path.
#
# Query implementation notes:
# - format_type includes the column size constraint, e.g. varchar(50)
# - ::regclass is a function that gives the id for a table name
def column_definitions(table_name) #:nodoc:
query <<-end_sql
SELECT a.attname, format_type(a.atttypid, a.atttypmod), d.adsrc, a.attnotnull
FROM pg_attribute a LEFT JOIN pg_attrdef d
ON a.attrelid = d.adrelid AND a.attnum = d.adnum
WHERE a.attrelid = '#{table_name}'::regclass
AND a.attnum > 0 AND NOT a.attisdropped
ORDER BY a.attnum
end_sql
end
end
end
end

View file

@ -1,34 +0,0 @@
require 'active_record/connection_adapters/sqlite_adapter'
module ActiveRecord
class Base
# sqlite3 adapter reuses sqlite_connection.
def self.sqlite3_connection(config) # :nodoc:
parse_sqlite_config!(config)
unless self.class.const_defined?(:SQLite3)
require_library_or_gem(config[:adapter])
end
db = SQLite3::Database.new(
config[:database],
:results_as_hash => true,
:type_translation => false
)
db.busy_timeout(config[:timeout]) unless config[:timeout].nil?
ConnectionAdapters::SQLite3Adapter.new(db, logger)
end
end
module ConnectionAdapters #:nodoc:
class SQLite3Adapter < SQLiteAdapter # :nodoc:
def table_structure(table_name)
returning structure = @connection.table_info(quote_table_name(table_name)) do
raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if structure.empty?
end
end
end
end
end

View file

@ -1,406 +0,0 @@
require 'active_record/connection_adapters/abstract_adapter'
module ActiveRecord
class Base
class << self
# Establishes a connection to the database that's used by all Active Record objects
def sqlite_connection(config) # :nodoc:
parse_sqlite_config!(config)
unless self.class.const_defined?(:SQLite)
require_library_or_gem(config[:adapter])
db = SQLite::Database.new(config[:database], 0)
db.show_datatypes = "ON" if !defined? SQLite::Version
db.results_as_hash = true if defined? SQLite::Version
db.type_translation = false
# "Downgrade" deprecated sqlite API
if SQLite.const_defined?(:Version)
ConnectionAdapters::SQLite2Adapter.new(db, logger)
else
ConnectionAdapters::DeprecatedSQLiteAdapter.new(db, logger)
end
end
end
private
def parse_sqlite_config!(config)
config[:database] ||= config[:dbfile]
# Require database.
unless config[:database]
raise ArgumentError, "No database file specified. Missing argument: database"
end
# Allow database path relative to RAILS_ROOT, but only if
# the database path is not the special path that tells
# Sqlite to build a database only in memory.
if Object.const_defined?(:RAILS_ROOT) && ':memory:' != config[:database]
config[:database] = File.expand_path(config[:database], RAILS_ROOT)
end
end
end
end
module ConnectionAdapters #:nodoc:
class SQLiteColumn < Column #:nodoc:
class << self
def string_to_binary(value)
value.gsub(/\0|\%/n) do |b|
case b
when "\0" then "%00"
when "%" then "%25"
end
end
end
def binary_to_string(value)
value.gsub(/%00|%25/n) do |b|
case b
when "%00" then "\0"
when "%25" then "%"
end
end
end
end
end
# The SQLite adapter works with both the 2.x and 3.x series of SQLite with the sqlite-ruby drivers (available both as gems and
# from http://rubyforge.org/projects/sqlite-ruby/).
#
# Options:
#
# * <tt>:database</tt> - Path to the database file.
class SQLiteAdapter < AbstractAdapter
def adapter_name #:nodoc:
'SQLite'
end
def supports_migrations? #:nodoc:
true
end
def requires_reloading?
true
end
def disconnect!
super
@connection.close rescue nil
end
def supports_count_distinct? #:nodoc:
sqlite_version >= '3.2.6'
end
def supports_autoincrement? #:nodoc:
sqlite_version >= '3.1.0'
end
def native_database_types #:nodoc:
{
:primary_key => default_primary_key_type,
:string => { :name => "varchar", :limit => 255 },
:text => { :name => "text" },
:integer => { :name => "integer" },
:float => { :name => "float" },
:decimal => { :name => "decimal" },
:datetime => { :name => "datetime" },
:timestamp => { :name => "datetime" },
:time => { :name => "time" },
:date => { :name => "date" },
:binary => { :name => "blob" },
:boolean => { :name => "boolean" }
}
end
# QUOTING ==================================================
def quote_string(s) #:nodoc:
@connection.class.quote(s)
end
def quote_column_name(name) #:nodoc:
%Q("#{name}")
end
# DATABASE STATEMENTS ======================================
def execute(sql, name = nil) #:nodoc:
catch_schema_changes { log(sql, name) { @connection.execute(sql) } }
end
def update_sql(sql, name = nil) #:nodoc:
super
@connection.changes
end
def delete_sql(sql, name = nil) #:nodoc:
sql += " WHERE 1=1" unless sql =~ /WHERE/i
super sql, name
end
def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc:
super || @connection.last_insert_row_id
end
def select_rows(sql, name = nil)
execute(sql, name).map do |row|
(0...(row.size / 2)).map { |i| row[i] }
end
end
def begin_db_transaction #:nodoc:
catch_schema_changes { @connection.transaction }
end
def commit_db_transaction #:nodoc:
catch_schema_changes { @connection.commit }
end
def rollback_db_transaction #:nodoc:
catch_schema_changes { @connection.rollback }
end
# SELECT ... FOR UPDATE is redundant since the table is locked.
def add_lock!(sql, options) #:nodoc:
sql
end
# SCHEMA STATEMENTS ========================================
def tables(name = nil) #:nodoc:
sql = <<-SQL
SELECT name
FROM sqlite_master
WHERE type = 'table' AND NOT name = 'sqlite_sequence'
SQL
execute(sql, name).map do |row|
row[0]
end
end
def columns(table_name, name = nil) #:nodoc:
table_structure(table_name).map do |field|
SQLiteColumn.new(field['name'], field['dflt_value'], field['type'], field['notnull'] == "0")
end
end
def indexes(table_name, name = nil) #:nodoc:
execute("PRAGMA index_list(#{quote_table_name(table_name)})", name).map do |row|
index = IndexDefinition.new(table_name, row['name'])
index.unique = row['unique'] != '0'
index.columns = execute("PRAGMA index_info('#{index.name}')").map { |col| col['name'] }
index
end
end
def primary_key(table_name) #:nodoc:
column = table_structure(table_name).find {|field| field['pk'].to_i == 1}
column ? column['name'] : nil
end
def remove_index(table_name, options={}) #:nodoc:
execute "DROP INDEX #{quote_column_name(index_name(table_name, options))}"
end
def rename_table(name, new_name)
execute "ALTER TABLE #{name} RENAME TO #{new_name}"
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
super(table_name, column_name, type, options)
# See last paragraph on http://www.sqlite.org/lang_altertable.html
execute "VACUUM"
end
def remove_column(table_name, *column_names) #:nodoc:
column_names.flatten.each do |column_name|
alter_table(table_name) do |definition|
definition.columns.delete(definition[column_name])
end
end
end
alias :remove_columns :remove_column
def change_column_default(table_name, column_name, default) #:nodoc:
alter_table(table_name) do |definition|
definition[column_name].default = default
end
end
def change_column(table_name, column_name, type, options = {}) #:nodoc:
alter_table(table_name) do |definition|
include_default = options_include_default?(options)
definition[column_name].instance_eval do
self.type = type
self.limit = options[:limit] if options.include?(:limit)
self.default = options[:default] if include_default
self.null = options[:null] if options.include?(:null)
end
end
end
def rename_column(table_name, column_name, new_column_name) #:nodoc:
alter_table(table_name, :rename => {column_name.to_s => new_column_name.to_s})
end
def empty_insert_statement(table_name)
"INSERT INTO #{table_name} VALUES(NULL)"
end
protected
def select(sql, name = nil) #:nodoc:
execute(sql, name).map do |row|
record = {}
row.each_key do |key|
if key.is_a?(String)
record[key.sub(/^"?\w+"?\./, '')] = row[key]
end
end
record
end
end
def table_structure(table_name)
returning structure = execute("PRAGMA table_info(#{quote_table_name(table_name)})") do
raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if structure.empty?
end
end
def alter_table(table_name, options = {}) #:nodoc:
altered_table_name = "altered_#{table_name}"
caller = lambda {|definition| yield definition if block_given?}
transaction do
move_table(table_name, altered_table_name,
options.merge(:temporary => true))
move_table(altered_table_name, table_name, &caller)
end
end
def move_table(from, to, options = {}, &block) #:nodoc:
copy_table(from, to, options, &block)
drop_table(from)
end
def copy_table(from, to, options = {}) #:nodoc:
options = options.merge(:id => !columns(from).detect{|c| c.name == 'id'}.nil?)
create_table(to, options) do |definition|
@definition = definition
columns(from).each do |column|
column_name = options[:rename] ?
(options[:rename][column.name] ||
options[:rename][column.name.to_sym] ||
column.name) : column.name
@definition.column(column_name, column.type,
:limit => column.limit, :default => column.default,
:null => column.null)
end
@definition.primary_key(primary_key(from)) if primary_key(from)
yield @definition if block_given?
end
copy_table_indexes(from, to, options[:rename] || {})
copy_table_contents(from, to,
@definition.columns.map {|column| column.name},
options[:rename] || {})
end
def copy_table_indexes(from, to, rename = {}) #:nodoc:
indexes(from).each do |index|
name = index.name
if to == "altered_#{from}"
name = "temp_#{name}"
elsif from == "altered_#{to}"
name = name[5..-1]
end
to_column_names = columns(to).map(&:name)
columns = index.columns.map {|c| rename[c] || c }.select do |column|
to_column_names.include?(column)
end
unless columns.empty?
# index name can't be the same
opts = { :name => name.gsub(/_(#{from})_/, "_#{to}_") }
opts[:unique] = true if index.unique
add_index(to, columns, opts)
end
end
end
def copy_table_contents(from, to, columns, rename = {}) #:nodoc:
column_mappings = Hash[*columns.map {|name| [name, name]}.flatten]
rename.inject(column_mappings) {|map, a| map[a.last] = a.first; map}
from_columns = columns(from).collect {|col| col.name}
columns = columns.find_all{|col| from_columns.include?(column_mappings[col])}
quoted_columns = columns.map { |col| quote_column_name(col) } * ','
quoted_to = quote_table_name(to)
@connection.execute "SELECT * FROM #{quote_table_name(from)}" do |row|
sql = "INSERT INTO #{quoted_to} (#{quoted_columns}) VALUES ("
sql << columns.map {|col| quote row[column_mappings[col]]} * ', '
sql << ')'
@connection.execute sql
end
end
def catch_schema_changes
return yield
rescue ActiveRecord::StatementInvalid => exception
if exception.message =~ /database schema has changed/
reconnect!
retry
else
raise
end
end
def sqlite_version
@sqlite_version ||= select_value('select sqlite_version(*)')
end
def default_primary_key_type
if supports_autoincrement?
'INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL'.freeze
else
'INTEGER PRIMARY KEY NOT NULL'.freeze
end
end
end
class SQLite2Adapter < SQLiteAdapter # :nodoc:
def supports_count_distinct? #:nodoc:
false
end
def rename_table(name, new_name)
move_table(name, new_name)
end
def add_column(table_name, column_name, type, options = {}) #:nodoc:
alter_table(table_name) do |definition|
definition.column(column_name, type, options)
end
end
end
class DeprecatedSQLiteAdapter < SQLite2Adapter # :nodoc:
def insert(sql, name = nil, pk = nil, id_value = nil)
execute(sql, name = nil)
id_value || @connection.last_insert_rowid
end
end
end
end

View file

@ -1,158 +0,0 @@
module ActiveRecord
# Track unsaved attribute changes.
#
# A newly instantiated object is unchanged:
# person = Person.find_by_name('uncle bob')
# person.changed? # => false
#
# Change the name:
# person.name = 'Bob'
# person.changed? # => true
# person.name_changed? # => true
# person.name_was # => 'uncle bob'
# person.name_change # => ['uncle bob', 'Bob']
# person.name = 'Bill'
# person.name_change # => ['uncle bob', 'Bill']
#
# Save the changes:
# person.save
# person.changed? # => false
# person.name_changed? # => false
#
# Assigning the same value leaves the attribute unchanged:
# person.name = 'Bill'
# person.name_changed? # => false
# person.name_change # => nil
#
# Which attributes have changed?
# person.name = 'bob'
# person.changed # => ['name']
# person.changes # => { 'name' => ['Bill', 'bob'] }
#
# Before modifying an attribute in-place:
# person.name_will_change!
# person.name << 'by'
# person.name_change # => ['uncle bob', 'uncle bobby']
module Dirty
def self.included(base)
base.attribute_method_suffix '_changed?', '_change', '_will_change!', '_was'
base.alias_method_chain :write_attribute, :dirty
base.alias_method_chain :save, :dirty
base.alias_method_chain :save!, :dirty
base.alias_method_chain :update, :dirty
base.alias_method_chain :reload, :dirty
base.superclass_delegating_accessor :partial_updates
base.partial_updates = true
end
# Do any attributes have unsaved changes?
# person.changed? # => false
# person.name = 'bob'
# person.changed? # => true
def changed?
!changed_attributes.empty?
end
# List of attributes with unsaved changes.
# person.changed # => []
# person.name = 'bob'
# person.changed # => ['name']
def changed
changed_attributes.keys
end
# Map of changed attrs => [original value, new value]
# person.changes # => {}
# person.name = 'bob'
# person.changes # => { 'name' => ['bill', 'bob'] }
def changes
changed.inject({}) { |h, attr| h[attr] = attribute_change(attr); h }
end
# Attempts to +save+ the record and clears changed attributes if successful.
def save_with_dirty(*args) #:nodoc:
if status = save_without_dirty(*args)
changed_attributes.clear
end
status
end
# Attempts to <tt>save!</tt> the record and clears changed attributes if successful.
def save_with_dirty!(*args) #:nodoc:
status = save_without_dirty!(*args)
changed_attributes.clear
status
end
# <tt>reload</tt> the record and clears changed attributes.
def reload_with_dirty(*args) #:nodoc:
record = reload_without_dirty(*args)
changed_attributes.clear
record
end
private
# Map of change attr => original value.
def changed_attributes
@changed_attributes ||= {}
end
# Handle *_changed? for method_missing.
def attribute_changed?(attr)
changed_attributes.include?(attr)
end
# Handle *_change for method_missing.
def attribute_change(attr)
[changed_attributes[attr], __send__(attr)] if attribute_changed?(attr)
end
# Handle *_was for method_missing.
def attribute_was(attr)
attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr)
end
# Handle *_will_change! for method_missing.
def attribute_will_change!(attr)
changed_attributes[attr] = clone_attribute_value(:read_attribute, attr)
end
# Wrap write_attribute to remember original attribute value.
def write_attribute_with_dirty(attr, value)
attr = attr.to_s
# The attribute already has an unsaved change.
unless changed_attributes.include?(attr)
old = clone_attribute_value(:read_attribute, attr)
changed_attributes[attr] = old if field_changed?(attr, old, value)
end
# Carry on.
write_attribute_without_dirty(attr, value)
end
def update_with_dirty
if partial_updates?
update_without_dirty(changed)
else
update_without_dirty
end
end
def field_changed?(attr, old, value)
if column = column_for_attribute(attr)
if column.type == :integer && column.null && old.nil?
# For nullable integer 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 ''.
value = nil if value.blank?
else
value = column.type_cast(value)
end
end
old != value
end
end
end

View file

@ -1,997 +0,0 @@
require 'erb'
require 'yaml'
require 'csv'
require 'active_support/test_case'
if RUBY_VERSION < '1.9'
module YAML #:nodoc:
class Omap #:nodoc:
def keys; map { |k, v| k } end
def values; map { |k, v| v } end
end
end
end
if defined? ActiveRecord
class FixtureClassNotFound < ActiveRecord::ActiveRecordError #:nodoc:
end
else
class FixtureClassNotFound < StandardError #:nodoc:
end
end
# Fixtures are a way of organizing data that you want to test against; in short, sample data. They come in 3 flavors:
#
# 1. YAML fixtures
# 2. CSV fixtures
# 3. Single-file fixtures
#
# = YAML fixtures
#
# This type of fixture is in YAML format and the preferred default. YAML is a file format which describes data structures
# in a non-verbose, human-readable format. It ships with Ruby 1.8.1+.
#
# Unlike single-file fixtures, YAML fixtures are stored in a single file per model, which are placed in the directory appointed
# by <tt>ActiveSupport::TestCase.fixture_path=(path)</tt> (this is automatically configured for Rails, so you can just
# put your files in <tt><your-rails-app>/test/fixtures/</tt>). The fixture file ends with the <tt>.yml</tt> file extension (Rails example:
# <tt><your-rails-app>/test/fixtures/web_sites.yml</tt>). The format of a YAML fixture file looks like this:
#
# rubyonrails:
# id: 1
# name: Ruby on Rails
# url: http://www.rubyonrails.org
#
# google:
# id: 2
# name: Google
# url: http://www.google.com
#
# This YAML fixture file includes two fixtures. Each YAML fixture (ie. record) is given a name and is followed by an
# indented list of key/value pairs in the "key: value" format. Records are separated by a blank line for your viewing
# pleasure.
#
# Note that YAML fixtures are unordered. If you want ordered fixtures, use the omap YAML type. See http://yaml.org/type/omap.html
# for the specification. You will need ordered fixtures when you have foreign key constraints on keys in the same table.
# This is commonly needed for tree structures. Example:
#
# --- !omap
# - parent:
# id: 1
# parent_id: NULL
# title: Parent
# - child:
# id: 2
# parent_id: 1
# title: Child
#
# = CSV fixtures
#
# Fixtures can also be kept in the Comma Separated Value format. Akin to YAML fixtures, CSV fixtures are stored
# in a single file, but instead end with the <tt>.csv</tt> file extension
# (Rails example: <tt><your-rails-app>/test/fixtures/web_sites.csv</tt>).
#
# The format of this type of fixture file is much more compact than the others, but also a little harder to read by us
# humans. The first line of the CSV file is a comma-separated list of field names. The rest of the file is then comprised
# of the actual data (1 per line). Here's an example:
#
# id, name, url
# 1, Ruby On Rails, http://www.rubyonrails.org
# 2, Google, http://www.google.com
#
# Should you have a piece of data with a comma character in it, you can place double quotes around that value. If you
# need to use a double quote character, you must escape it with another double quote.
#
# Another unique attribute of the CSV fixture is that it has *no* fixture name like the other two formats. Instead, the
# fixture names are automatically generated by deriving the class name of the fixture file and adding an incrementing
# number to the end. In our example, the 1st fixture would be called "web_site_1" and the 2nd one would be called
# "web_site_2".
#
# Most databases and spreadsheets support exporting to CSV format, so this is a great format for you to choose if you
# have existing data somewhere already.
#
# = Single-file fixtures
#
# This type of fixture was the original format for Active Record that has since been deprecated in favor of the YAML and CSV formats.
# Fixtures for this format are created by placing text files in a sub-directory (with the name of the model) to the directory
# appointed by <tt>ActiveSupport::TestCase.fixture_path=(path)</tt> (this is automatically configured for Rails, so you can just
# put your files in <tt><your-rails-app>/test/fixtures/<your-model-name>/</tt> --
# like <tt><your-rails-app>/test/fixtures/web_sites/</tt> for the WebSite model).
#
# Each text file placed in this directory represents a "record". Usually these types of fixtures are named without
# extensions, but if you are on a Windows machine, you might consider adding <tt>.txt</tt> as the extension. Here's what the
# above example might look like:
#
# web_sites/google
# web_sites/yahoo.txt
# web_sites/ruby-on-rails
#
# The file format of a standard fixture is simple. Each line is a property (or column in db speak) and has the syntax
# of "name => value". Here's an example of the ruby-on-rails fixture above:
#
# id => 1
# name => Ruby on Rails
# url => http://www.rubyonrails.org
#
# = Using Fixtures
#
# Since fixtures are a testing construct, we use them in our unit and functional tests. There are two ways to use the
# fixtures, but first let's take a look at a sample unit test:
#
# require 'web_site'
#
# class WebSiteTest < ActiveSupport::TestCase
# def test_web_site_count
# assert_equal 2, WebSite.count
# end
# end
#
# As it stands, unless we pre-load the web_site table in our database with two records, this test will fail. Here's the
# easiest way to add fixtures to the database:
#
# ...
# class WebSiteTest < ActiveSupport::TestCase
# fixtures :web_sites # add more by separating the symbols with commas
# ...
#
# By adding a "fixtures" method to the test case and passing it a list of symbols (only one is shown here though), we trigger
# the testing environment to automatically load the appropriate fixtures into the database before each test.
# To ensure consistent data, the environment deletes the fixtures before running the load.
#
# In addition to being available in the database, the fixtures are also loaded into a hash stored in an instance variable
# of the test case. It is named after the symbol... so, in our example, there would be a hash available called
# <tt>@web_sites</tt>. This is where the "fixture name" comes into play.
#
# On top of that, each record is automatically "found" (using <tt>Model.find(id)</tt>) and placed in the instance variable of its name.
# So for the YAML fixtures, we'd get <tt>@rubyonrails</tt> and <tt>@google</tt>, which could be interrogated using regular Active Record semantics:
#
# # test if the object created from the fixture data has the same attributes as the data itself
# def test_find
# assert_equal @web_sites["rubyonrails"]["name"], @rubyonrails.name
# end
#
# As seen above, the data hash created from the YAML fixtures would have <tt>@web_sites["rubyonrails"]["url"]</tt> return
# "http://www.rubyonrails.org" and <tt>@web_sites["google"]["name"]</tt> would return "Google". The same fixtures, but loaded
# from a CSV fixture file, would be accessible via <tt>@web_sites["web_site_1"]["name"] == "Ruby on Rails"</tt> and have the individual
# fixtures available as instance variables <tt>@web_site_1</tt> and <tt>@web_site_2</tt>.
#
# If you do not wish to use instantiated fixtures (usually for performance reasons) there are two options.
#
# - to completely disable instantiated fixtures:
# self.use_instantiated_fixtures = false
#
# - to keep the fixture instance (@web_sites) available, but do not automatically 'find' each instance:
# self.use_instantiated_fixtures = :no_instances
#
# Even if auto-instantiated fixtures are disabled, you can still access them
# by name via special dynamic methods. Each method has the same name as the
# model, and accepts the name of the fixture to instantiate:
#
# fixtures :web_sites
#
# def test_find
# assert_equal "Ruby on Rails", web_sites(:rubyonrails).name
# end
#
# = Dynamic fixtures with ERb
#
# Some times you don't care about the content of the fixtures as much as you care about the volume. In these cases, you can
# mix ERb in with your YAML or CSV fixtures to create a bunch of fixtures for load testing, like:
#
# <% for i in 1..1000 %>
# fix_<%= i %>:
# id: <%= i %>
# name: guy_<%= 1 %>
# <% end %>
#
# This will create 1000 very simple YAML fixtures.
#
# Using ERb, you can also inject dynamic values into your fixtures with inserts like <tt><%= Date.today.strftime("%Y-%m-%d") %></tt>.
# This is however a feature to be used with some caution. The point of fixtures are that they're stable units of predictable
# sample data. If you feel that you need to inject dynamic values, then perhaps you should reexamine whether your application
# is properly testable. Hence, dynamic values in fixtures are to be considered a code smell.
#
# = Transactional fixtures
#
# TestCases can use begin+rollback to isolate their changes to the database instead of having to delete+insert for every test case.
# They can also turn off auto-instantiation of fixture data since the feature is costly and often unused.
#
# class FooTest < ActiveSupport::TestCase
# self.use_transactional_fixtures = true
# self.use_instantiated_fixtures = false
#
# fixtures :foos
#
# def test_godzilla
# assert !Foo.find(:all).empty?
# Foo.destroy_all
# assert Foo.find(:all).empty?
# end
#
# def test_godzilla_aftermath
# assert !Foo.find(:all).empty?
# end
# end
#
# If you preload your test database with all fixture data (probably in the Rakefile task) and use transactional fixtures,
# then you may omit all fixtures declarations in your test cases since all the data's already there and every case rolls back its changes.
#
# In order to use instantiated fixtures with preloaded data, set +self.pre_loaded_fixtures+ to true. This will provide
# access to fixture data for every table that has been loaded through fixtures (depending on the value of +use_instantiated_fixtures+)
#
# When *not* to use transactional fixtures:
# 1. You're testing whether a transaction works correctly. Nested transactions don't commit until all parent transactions commit,
# particularly, the fixtures transaction which is begun in setup and rolled back in teardown. Thus, you won't be able to verify
# the results of your transaction until Active Record supports nested transactions or savepoints (in progress).
# 2. Your database does not support transactions. Every Active Record database supports transactions except MySQL MyISAM.
# Use InnoDB, MaxDB, or NDB instead.
#
# = Advanced YAML Fixtures
#
# YAML fixtures that don't specify an ID get some extra features:
#
# * Stable, autogenerated ID's
# * Label references for associations (belongs_to, has_one, has_many)
# * HABTM associations as inline lists
# * Autofilled timestamp columns
# * Fixture label interpolation
# * Support for YAML defaults
#
# == Stable, autogenerated ID's
#
# Here, have a monkey fixture:
#
# george:
# id: 1
# name: George the Monkey
#
# reginald:
# id: 2
# name: Reginald the Pirate
#
# Each of these fixtures has two unique identifiers: one for the database
# and one for the humans. Why don't we generate the primary key instead?
# Hashing each fixture's label yields a consistent ID:
#
# george: # generated id: 503576764
# name: George the Monkey
#
# reginald: # generated id: 324201669
# name: Reginald the Pirate
#
# Active Record looks at the fixture's model class, discovers the correct
# primary key, and generates it right before inserting the fixture
# into the database.
#
# The generated ID for a given label is constant, so we can discover
# any fixture's ID without loading anything, as long as we know the label.
#
# == Label references for associations (belongs_to, has_one, has_many)
#
# Specifying foreign keys in fixtures can be very fragile, not to
# mention difficult to read. Since Active Record can figure out the ID of
# any fixture from its label, you can specify FK's by label instead of ID.
#
# === belongs_to
#
# Let's break out some more monkeys and pirates.
#
# ### in pirates.yml
#
# reginald:
# id: 1
# name: Reginald the Pirate
# monkey_id: 1
#
# ### in monkeys.yml
#
# george:
# id: 1
# name: George the Monkey
# pirate_id: 1
#
# Add a few more monkeys and pirates and break this into multiple files,
# and it gets pretty hard to keep track of what's going on. Let's
# use labels instead of ID's:
#
# ### in pirates.yml
#
# reginald:
# name: Reginald the Pirate
# monkey: george
#
# ### in monkeys.yml
#
# george:
# name: George the Monkey
# pirate: reginald
#
# Pow! All is made clear. Active Record reflects on the fixture's model class,
# finds all the +belongs_to+ associations, and allows you to specify
# a target *label* for the *association* (monkey: george) rather than
# a target *id* for the *FK* (<tt>monkey_id: 1</tt>).
#
# ==== Polymorphic belongs_to
#
# Supporting polymorphic relationships is a little bit more complicated, since
# Active Record needs to know what type your association is pointing at. Something
# like this should look familiar:
#
# ### in fruit.rb
#
# belongs_to :eater, :polymorphic => true
#
# ### in fruits.yml
#
# apple:
# id: 1
# name: apple
# eater_id: 1
# eater_type: Monkey
#
# Can we do better? You bet!
#
# apple:
# eater: george (Monkey)
#
# Just provide the polymorphic target type and Active Record will take care of the rest.
#
# === has_and_belongs_to_many
#
# Time to give our monkey some fruit.
#
# ### in monkeys.yml
#
# george:
# id: 1
# name: George the Monkey
# pirate_id: 1
#
# ### in fruits.yml
#
# apple:
# id: 1
# name: apple
#
# orange:
# id: 2
# name: orange
#
# grape:
# id: 3
# name: grape
#
# ### in fruits_monkeys.yml
#
# apple_george:
# fruit_id: 1
# monkey_id: 1
#
# orange_george:
# fruit_id: 2
# monkey_id: 1
#
# grape_george:
# fruit_id: 3
# monkey_id: 1
#
# Let's make the HABTM fixture go away.
#
# ### in monkeys.yml
#
# george:
# name: George the Monkey
# pirate: reginald
# fruits: apple, orange, grape
#
# ### in fruits.yml
#
# apple:
# name: apple
#
# orange:
# name: orange
#
# grape:
# name: grape
#
# Zap! No more fruits_monkeys.yml file. We've specified the list of fruits
# on George's fixture, but we could've just as easily specified a list
# of monkeys on each fruit. As with +belongs_to+, Active Record reflects on
# the fixture's model class and discovers the +has_and_belongs_to_many+
# associations.
#
# == Autofilled timestamp columns
#
# If your table/model specifies any of Active Record's
# standard timestamp columns (+created_at+, +created_on+, +updated_at+, +updated_on+),
# they will automatically be set to <tt>Time.now</tt>.
#
# If you've set specific values, they'll be left alone.
#
# == Fixture label interpolation
#
# The label of the current fixture is always available as a column value:
#
# geeksomnia:
# name: Geeksomnia's Account
# subdomain: $LABEL
#
# Also, sometimes (like when porting older join table fixtures) you'll need
# to be able to get ahold of the identifier for a given label. ERB
# to the rescue:
#
# george_reginald:
# monkey_id: <%= Fixtures.identify(:reginald) %>
# pirate_id: <%= Fixtures.identify(:george) %>
#
# == Support for YAML defaults
#
# You probably already know how to use YAML to set and reuse defaults in
# your <tt>database.yml</tt> file. You can use the same technique in your fixtures:
#
# DEFAULTS: &DEFAULTS
# created_on: <%= 3.weeks.ago.to_s(:db) %>
#
# first:
# name: Smurf
# <<: *DEFAULTS
#
# second:
# name: Fraggle
# <<: *DEFAULTS
#
# Any fixture labeled "DEFAULTS" is safely ignored.
class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash)
DEFAULT_FILTER_RE = /\.ya?ml$/
@@all_cached_fixtures = {}
def self.reset_cache(connection = nil)
connection ||= ActiveRecord::Base.connection
@@all_cached_fixtures[connection.object_id] = {}
end
def self.cache_for_connection(connection)
@@all_cached_fixtures[connection.object_id] ||= {}
@@all_cached_fixtures[connection.object_id]
end
def self.fixture_is_cached?(connection, table_name)
cache_for_connection(connection)[table_name]
end
def self.cached_fixtures(connection, keys_to_fetch = nil)
if keys_to_fetch
fixtures = cache_for_connection(connection).values_at(*keys_to_fetch)
else
fixtures = cache_for_connection(connection).values
end
fixtures.size > 1 ? fixtures : fixtures.first
end
def self.cache_fixtures(connection, fixtures_map)
cache_for_connection(connection).update(fixtures_map)
end
def self.instantiate_fixtures(object, table_name, fixtures, load_instances = true)
object.instance_variable_set "@#{table_name.to_s.gsub('.','_')}", fixtures
if load_instances
ActiveRecord::Base.silence do
fixtures.each do |name, fixture|
begin
object.instance_variable_set "@#{name}", fixture.find
rescue FixtureClassNotFound
nil
end
end
end
end
end
def self.instantiate_all_loaded_fixtures(object, load_instances = true)
all_loaded_fixtures.each do |table_name, fixtures|
Fixtures.instantiate_fixtures(object, table_name, fixtures, load_instances)
end
end
cattr_accessor :all_loaded_fixtures
self.all_loaded_fixtures = {}
def self.create_fixtures(fixtures_directory, table_names, class_names = {})
table_names = [table_names].flatten.map { |n| n.to_s }
connection = block_given? ? yield : ActiveRecord::Base.connection
table_names_to_fetch = table_names.reject { |table_name| fixture_is_cached?(connection, table_name) }
unless table_names_to_fetch.empty?
ActiveRecord::Base.silence do
connection.disable_referential_integrity do
fixtures_map = {}
fixtures = table_names_to_fetch.map do |table_name|
fixtures_map[table_name] = Fixtures.new(connection, File.split(table_name.to_s).last, class_names[table_name.to_sym], File.join(fixtures_directory, table_name.to_s))
end
all_loaded_fixtures.update(fixtures_map)
connection.transaction(Thread.current['open_transactions'].to_i == 0) do
fixtures.reverse.each { |fixture| fixture.delete_existing_fixtures }
fixtures.each { |fixture| fixture.insert_fixtures }
# Cap primary key sequences to max(pk).
if connection.respond_to?(:reset_pk_sequence!)
table_names.each do |table_name|
connection.reset_pk_sequence!(table_name)
end
end
end
cache_fixtures(connection, fixtures_map)
end
end
end
cached_fixtures(connection, table_names)
end
# Returns a consistent identifier for +label+. This will always
# be a positive integer, and will always be the same for a given
# label, assuming the same OS, platform, and version of Ruby.
def self.identify(label)
label.to_s.hash.abs
end
attr_reader :table_name
def initialize(connection, table_name, class_name, fixture_path, file_filter = DEFAULT_FILTER_RE)
@connection, @table_name, @fixture_path, @file_filter = connection, table_name, fixture_path, file_filter
@class_name = class_name ||
(ActiveRecord::Base.pluralize_table_names ? @table_name.singularize.camelize : @table_name.camelize)
@table_name = ActiveRecord::Base.table_name_prefix + @table_name + ActiveRecord::Base.table_name_suffix
@table_name = class_name.table_name if class_name.respond_to?(:table_name)
@connection = class_name.connection if class_name.respond_to?(:connection)
read_fixture_files
end
def delete_existing_fixtures
@connection.delete "DELETE FROM #{@connection.quote_table_name(table_name)}", 'Fixture Delete'
end
def insert_fixtures
now = ActiveRecord::Base.default_timezone == :utc ? Time.now.utc : Time.now
now = now.to_s(:db)
# allow a standard key to be used for doing defaults in YAML
if is_a?(Hash)
delete('DEFAULTS')
else
delete(assoc('DEFAULTS'))
end
# track any join tables we need to insert later
habtm_fixtures = Hash.new do |h, habtm|
h[habtm] = HabtmFixtures.new(@connection, habtm.options[:join_table], nil, nil)
end
each do |label, fixture|
row = fixture.to_hash
if model_class && model_class < ActiveRecord::Base
# fill in timestamp columns if they aren't specified and the model is set to record_timestamps
if model_class.record_timestamps
timestamp_column_names.each do |name|
row[name] = now unless row.key?(name)
end
end
# interpolate the fixture label
row.each do |key, value|
row[key] = label if value == "$LABEL"
end
# generate a primary key if necessary
if has_primary_key_column? && !row.include?(primary_key_name)
row[primary_key_name] = Fixtures.identify(label)
end
# If STI is used, find the correct subclass for association reflection
reflection_class =
if row.include?(inheritance_column_name)
row[inheritance_column_name].constantize rescue model_class
else
model_class
end
reflection_class.reflect_on_all_associations.each do |association|
case association.macro
when :belongs_to
# Do not replace association name with association foreign key if they are named the same
fk_name = (association.options[:foreign_key] || "#{association.name}_id").to_s
if association.name.to_s != fk_name && value = row.delete(association.name.to_s)
if association.options[:polymorphic]
if value.sub!(/\s*\(([^\)]*)\)\s*$/, "")
target_type = $1
target_type_name = (association.options[:foreign_type] || "#{association.name}_type").to_s
# support polymorphic belongs_to as "label (Type)"
row[target_type_name] = target_type
end
end
row[fk_name] = Fixtures.identify(value)
end
when :has_and_belongs_to_many
if (targets = row.delete(association.name.to_s))
targets = targets.is_a?(Array) ? targets : targets.split(/\s*,\s*/)
join_fixtures = habtm_fixtures[association]
targets.each do |target|
join_fixtures["#{label}_#{target}"] = Fixture.new(
{ association.primary_key_name => row[primary_key_name],
association.association_foreign_key => Fixtures.identify(target) }, nil)
end
end
end
end
end
@connection.insert_fixture(fixture, @table_name)
end
# insert any HABTM join tables we discovered
habtm_fixtures.values.each do |fixture|
fixture.delete_existing_fixtures
fixture.insert_fixtures
end
end
private
class HabtmFixtures < ::Fixtures #:nodoc:
def read_fixture_files; end
end
def model_class
unless defined?(@model_class)
@model_class =
if @class_name.nil? || @class_name.is_a?(Class)
@class_name
else
@class_name.constantize rescue nil
end
end
@model_class
end
def primary_key_name
@primary_key_name ||= model_class && model_class.primary_key
end
def has_primary_key_column?
@has_primary_key_column ||= model_class && primary_key_name &&
model_class.columns.find { |c| c.name == primary_key_name }
end
def timestamp_column_names
@timestamp_column_names ||= %w(created_at created_on updated_at updated_on).select do |name|
column_names.include?(name)
end
end
def inheritance_column_name
@inheritance_column_name ||= model_class && model_class.inheritance_column
end
def column_names
@column_names ||= @connection.columns(@table_name).collect(&:name)
end
def read_fixture_files
if File.file?(yaml_file_path)
read_yaml_fixture_files
elsif File.file?(csv_file_path)
read_csv_fixture_files
end
end
def read_yaml_fixture_files
yaml_string = ""
Dir["#{@fixture_path}/**/*.yml"].select { |f| test(?f, f) }.each do |subfixture_path|
yaml_string << IO.read(subfixture_path)
end
yaml_string << IO.read(yaml_file_path)
if yaml = parse_yaml_string(yaml_string)
# If the file is an ordered map, extract its children.
yaml_value =
if yaml.respond_to?(:type_id) && yaml.respond_to?(:value)
yaml.value
else
[yaml]
end
yaml_value.each do |fixture|
raise Fixture::FormatError, "Bad data for #{@class_name} fixture named #{fixture}" unless fixture.respond_to?(:each)
fixture.each do |name, data|
unless data
raise Fixture::FormatError, "Bad data for #{@class_name} fixture named #{name} (nil)"
end
self[name] = Fixture.new(data, model_class)
end
end
end
end
def read_csv_fixture_files
reader = CSV.parse(erb_render(IO.read(csv_file_path)))
header = reader.shift
i = 0
reader.each do |row|
data = {}
row.each_with_index { |cell, j| data[header[j].to_s.strip] = cell.to_s.strip }
self["#{@class_name.to_s.underscore}_#{i+=1}"] = Fixture.new(data, model_class)
end
end
def yaml_file_path
"#{@fixture_path}.yml"
end
def csv_file_path
@fixture_path + ".csv"
end
def yaml_fixtures_key(path)
File.basename(@fixture_path).split(".").first
end
def parse_yaml_string(fixture_content)
YAML::load(erb_render(fixture_content))
rescue => error
raise Fixture::FormatError, "a YAML error occurred parsing #{yaml_file_path}. Please note that YAML must be consistently indented using spaces. Tabs are not allowed. Please have a look at http://www.yaml.org/faq.html\nThe exact error was:\n #{error.class}: #{error}"
end
def erb_render(fixture_content)
ERB.new(fixture_content).result
end
end
class Fixture #:nodoc:
include Enumerable
class FixtureError < StandardError #:nodoc:
end
class FormatError < FixtureError #:nodoc:
end
attr_reader :model_class
def initialize(fixture, model_class)
@fixture = fixture
@model_class = model_class.is_a?(Class) ? model_class : model_class.constantize rescue nil
end
def class_name
@model_class.name if @model_class
end
def each
@fixture.each { |item| yield item }
end
def [](key)
@fixture[key]
end
def to_hash
@fixture
end
def key_list
columns = @fixture.keys.collect{ |column_name| ActiveRecord::Base.connection.quote_column_name(column_name) }
columns.join(", ")
end
def value_list
list = @fixture.inject([]) do |fixtures, (key, value)|
col = model_class.columns_hash[key] if model_class.respond_to?(:ancestors) && model_class.ancestors.include?(ActiveRecord::Base)
fixtures << ActiveRecord::Base.connection.quote(value, col).gsub('[^\]\\n', "\n").gsub('[^\]\\r', "\r")
end
list * ', '
end
def find
if model_class
model_class.find(self[model_class.primary_key])
else
raise FixtureClassNotFound, "No class attached to find."
end
end
end
module Test #:nodoc:
module Unit #:nodoc:
class TestCase #:nodoc:
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
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
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.send :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? && Thread.current['open_transactions'] != 0
ActiveRecord::Base.connection.rollback_db_transaction
Thread.current['open_transactions'] = 0
end
ActiveRecord::Base.verify_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.table_name] = fixtures
else
fixtures.each { |f| @loaded_fixtures[f.table_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
end

View file

@ -1,147 +0,0 @@
module ActiveRecord
module Locking
# == What is Optimistic Locking
#
# Optimistic locking allows multiple users to access the same record for edits, and assumes a minimum of
# conflicts with the data. It does this by checking whether another process has made changes to a record since
# it was opened, an ActiveRecord::StaleObjectError is thrown if that has occurred and the update is ignored.
#
# Check out ActiveRecord::Locking::Pessimistic for an alternative.
#
# == Usage
#
# Active Records support optimistic locking if the field <tt>lock_version</tt> is present. Each update to the
# record increments the lock_version column and the locking facilities ensure that records instantiated twice
# will let the last one saved raise a StaleObjectError if the first was also updated. Example:
#
# p1 = Person.find(1)
# p2 = Person.find(1)
#
# p1.first_name = "Michael"
# p1.save
#
# p2.first_name = "should fail"
# p2.save # Raises a ActiveRecord::StaleObjectError
#
# You're then responsible for dealing with the conflict by rescuing the exception and either rolling back, merging,
# or otherwise apply the business logic needed to resolve the conflict.
#
# You must ensure that your database schema defaults the lock_version column to 0.
#
# This behavior can be turned off by setting <tt>ActiveRecord::Base.lock_optimistically = false</tt>.
# To override the name of the lock_version column, invoke the <tt>set_locking_column</tt> method.
# This method uses the same syntax as <tt>set_table_name</tt>
module Optimistic
def self.included(base) #:nodoc:
base.extend ClassMethods
base.cattr_accessor :lock_optimistically, :instance_writer => false
base.lock_optimistically = true
base.alias_method_chain :update, :lock
base.alias_method_chain :attributes_from_column_definition, :lock
class << base
alias_method :locking_column=, :set_locking_column
end
end
def locking_enabled? #:nodoc:
self.class.locking_enabled?
end
private
def attributes_from_column_definition_with_lock
result = attributes_from_column_definition_without_lock
# If the locking column has no default value set,
# start the lock version at zero. Note we can't use
# locking_enabled? at this point as @attributes may
# not have been initialized yet
if lock_optimistically && result.include?(self.class.locking_column)
result[self.class.locking_column] ||= 0
end
return result
end
def update_with_lock(attribute_names = @attributes.keys) #:nodoc:
return update_without_lock(attribute_names) unless locking_enabled?
lock_col = self.class.locking_column
previous_value = send(lock_col).to_i
send(lock_col + '=', previous_value + 1)
attribute_names += [lock_col]
attribute_names.uniq!
begin
affected_rows = connection.update(<<-end_sql, "#{self.class.name} Update with optimistic locking")
UPDATE #{self.class.quoted_table_name}
SET #{quoted_comma_pair_list(connection, attributes_with_quotes(false, false, attribute_names))}
WHERE #{self.class.primary_key} = #{quote_value(id)}
AND #{self.class.quoted_locking_column} = #{quote_value(previous_value)}
end_sql
unless affected_rows == 1
raise ActiveRecord::StaleObjectError, "Attempted to update a stale object"
end
affected_rows
# If something went wrong, revert the version.
rescue Exception
send(lock_col + '=', previous_value)
raise
end
end
module ClassMethods
DEFAULT_LOCKING_COLUMN = 'lock_version'
def self.extended(base)
class <<base
alias_method_chain :update_counters, :lock
end
end
# Is optimistic locking enabled for this table? Returns true if the
# +lock_optimistically+ flag is set to true (which it is, by default)
# and the table includes the +locking_column+ column (defaults to
# +lock_version+).
def locking_enabled?
lock_optimistically && columns_hash[locking_column]
end
# Set the column to use for optimistic locking. Defaults to +lock_version+.
def set_locking_column(value = nil, &block)
define_attr_method :locking_column, value, &block
value
end
# The version column used for optimistic locking. Defaults to +lock_version+.
def locking_column
reset_locking_column
end
# Quote the column name used for optimistic locking.
def quoted_locking_column
connection.quote_column_name(locking_column)
end
# Reset the column used for optimistic locking back to the +lock_version+ default.
def reset_locking_column
set_locking_column DEFAULT_LOCKING_COLUMN
end
# Make sure the lock version column gets updated when counters are
# updated.
def update_counters_with_lock(id, counters)
counters = counters.merge(locking_column => 1) if locking_enabled?
update_counters_without_lock(id, counters)
end
end
end
end
end

View file

@ -1,77 +0,0 @@
# Copyright (c) 2006 Shugo Maeda <shugo@ruby-lang.org>
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject
# to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
# ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
# CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
module ActiveRecord
module Locking
# Locking::Pessimistic provides support for row-level locking using
# SELECT ... FOR UPDATE and other lock types.
#
# Pass <tt>:lock => true</tt> to ActiveRecord::Base.find to obtain an exclusive
# lock on the selected rows:
# # select * from accounts where id=1 for update
# Account.find(1, :lock => true)
#
# Pass <tt>:lock => 'some locking clause'</tt> to give a database-specific locking clause
# of your own such as 'LOCK IN SHARE MODE' or 'FOR UPDATE NOWAIT'.
#
# Example:
# Account.transaction do
# # select * from accounts where name = 'shugo' limit 1 for update
# shugo = Account.find(:first, :conditions => "name = 'shugo'", :lock => true)
# yuko = Account.find(:first, :conditions => "name = 'yuko'", :lock => true)
# shugo.balance -= 100
# shugo.save!
# yuko.balance += 100
# yuko.save!
# end
#
# You can also use ActiveRecord::Base#lock! method to lock one record by id.
# This may be better if you don't need to lock every row. Example:
# Account.transaction do
# # select * from accounts where ...
# accounts = Account.find(:all, :conditions => ...)
# account1 = accounts.detect { |account| ... }
# account2 = accounts.detect { |account| ... }
# # select * from accounts where id=? for update
# account1.lock!
# account2.lock!
# account1.balance -= 100
# account1.save!
# account2.balance += 100
# account2.save!
# end
#
# Database-specific information on row locking:
# MySQL: http://dev.mysql.com/doc/refman/5.1/en/innodb-locking-reads.html
# PostgreSQL: http://www.postgresql.org/docs/8.1/interactive/sql-select.html#SQL-FOR-UPDATE-SHARE
module Pessimistic
# Obtain a row lock on this record. Reloads the record to obtain the requested
# lock. Pass an SQL locking clause to append the end of the SELECT statement
# or pass true for "FOR UPDATE" (the default, an exclusive row lock). Returns
# the locked record.
def lock!(lock = true)
reload(:lock => lock) unless new_record?
self
end
end
end
end

View file

@ -1,496 +0,0 @@
module ActiveRecord
class IrreversibleMigration < ActiveRecordError#:nodoc:
end
class DuplicateMigrationVersionError < ActiveRecordError#:nodoc:
def initialize(version)
super("Multiple migrations have the version number #{version}")
end
end
class DuplicateMigrationNameError < ActiveRecordError#:nodoc:
def initialize(name)
super("Multiple migrations have the name #{name}")
end
end
class UnknownMigrationVersionError < ActiveRecordError #:nodoc:
def initialize(version)
super("No migration with version number #{version}")
end
end
class IllegalMigrationNameError < ActiveRecordError#:nodoc:
def initialize(name)
super("Illegal name for migration file: #{name}\n\t(only lower case letters, numbers, and '_' allowed)")
end
end
# Migrations can manage the evolution of a schema used by several physical databases. It's a solution
# to the common problem of adding a field to make a new feature work in your local database, but being unsure of how to
# push that change to other developers and to the production server. With migrations, you can describe the transformations
# in self-contained classes that can be checked into version control systems and executed against another database that
# might be one, two, or five versions behind.
#
# Example of a simple migration:
#
# class AddSsl < ActiveRecord::Migration
# def self.up
# add_column :accounts, :ssl_enabled, :boolean, :default => 1
# end
#
# def self.down
# remove_column :accounts, :ssl_enabled
# end
# end
#
# This migration will add a boolean flag to the accounts table and remove it if you're backing out of the migration.
# It shows how all migrations have two class methods +up+ and +down+ that describes the transformations required to implement
# or remove the migration. These methods can consist of both the migration specific methods like add_column and remove_column,
# but may also contain regular Ruby code for generating data needed for the transformations.
#
# Example of a more complex migration that also needs to initialize data:
#
# class AddSystemSettings < ActiveRecord::Migration
# def self.up
# create_table :system_settings do |t|
# t.string :name
# t.string :label
# t.text :value
# t.string :type
# t.integer :position
# end
#
# SystemSetting.create :name => "notice", :label => "Use notice?", :value => 1
# end
#
# def self.down
# drop_table :system_settings
# end
# end
#
# This migration first adds the system_settings table, then creates the very first row in it using the Active Record model
# that relies on the table. It also uses the more advanced create_table syntax where you can specify a complete table schema
# in one block call.
#
# == Available transformations
#
# * <tt>create_table(name, options)</tt> Creates a table called +name+ and makes the table object available to a block
# that can then add columns to it, following the same format as add_column. See example above. The options hash is for
# fragments like "DEFAULT CHARSET=UTF-8" that are appended to the create table definition.
# * <tt>drop_table(name)</tt>: Drops the table called +name+.
# * <tt>rename_table(old_name, new_name)</tt>: Renames the table called +old_name+ to +new_name+.
# * <tt>add_column(table_name, column_name, type, options)</tt>: Adds a new column to the table called +table_name+
# named +column_name+ specified to be one of the following types:
# <tt>:string</tt>, <tt>:text</tt>, <tt>:integer</tt>, <tt>:float</tt>, <tt>:decimal</tt>, <tt>:datetime</tt>, <tt>:timestamp</tt>, <tt>:time</tt>,
# <tt>:date</tt>, <tt>:binary</tt>, <tt>:boolean</tt>. A default value can be specified by passing an
# +options+ hash like <tt>{ :default => 11 }</tt>. Other options include <tt>:limit</tt> and <tt>:null</tt> (e.g. <tt>{ :limit => 50, :null => false }</tt>)
# -- see ActiveRecord::ConnectionAdapters::TableDefinition#column for details.
# * <tt>rename_column(table_name, column_name, new_column_name)</tt>: Renames a column but keeps the type and content.
# * <tt>change_column(table_name, column_name, type, options)</tt>: Changes the column to a different type using the same
# parameters as add_column.
# * <tt>remove_column(table_name, column_name)</tt>: Removes the column named +column_name+ from the table called +table_name+.
# * <tt>add_index(table_name, column_names, options)</tt>: Adds a new index with the name of the column. Other options include
# <tt>:name</tt> and <tt>:unique</tt> (e.g. <tt>{ :name => "users_name_index", :unique => true }</tt>).
# * <tt>remove_index(table_name, index_name)</tt>: Removes the index specified by +index_name+.
#
# == Irreversible transformations
#
# Some transformations are destructive in a manner that cannot be reversed. Migrations of that kind should raise
# an <tt>ActiveRecord::IrreversibleMigration</tt> exception in their +down+ method.
#
# == Running migrations from within Rails
#
# The Rails package has several tools to help create and apply migrations.
#
# To generate a new migration, you can use
# script/generate migration MyNewMigration
#
# where MyNewMigration is the name of your migration. The generator will
# create an empty migration file <tt>nnn_my_new_migration.rb</tt> in the <tt>db/migrate/</tt>
# directory where <tt>nnn</tt> is the next largest migration number.
#
# You may then edit the <tt>self.up</tt> and <tt>self.down</tt> methods of
# MyNewMigration.
#
# There is a special syntactic shortcut to generate migrations that add fields to a table.
# script/generate migration add_fieldname_to_tablename fieldname:string
#
# This will generate the file <tt>nnn_add_fieldname_to_tablename</tt>, which will look like this:
# class AddFieldnameToTablename < ActiveRecord::Migration
# def self.up
# add_column :tablenames, :fieldname, :string
# end
#
# def self.down
# remove_column :tablenames, :fieldname
# end
# end
#
# 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.
#
# 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
# you wish to downgrade. If any of the migrations throw an
# <tt>ActiveRecord::IrreversibleMigration</tt> exception, that step will fail and you'll
# have some manual work to do.
#
# == Database support
#
# Migrations are currently supported in MySQL, PostgreSQL, SQLite,
# SQL Server, Sybase, and Oracle (all supported databases except DB2).
#
# == More examples
#
# Not all migrations change the schema. Some just fix the data:
#
# class RemoveEmptyTags < ActiveRecord::Migration
# def self.up
# Tag.find(:all).each { |tag| tag.destroy if tag.pages.empty? }
# end
#
# def self.down
# # not much we can do to restore deleted data
# raise ActiveRecord::IrreversibleMigration, "Can't recover the deleted tags"
# end
# end
#
# Others remove columns when they migrate up instead of down:
#
# class RemoveUnnecessaryItemAttributes < ActiveRecord::Migration
# def self.up
# remove_column :items, :incomplete_items_count
# remove_column :items, :completed_items_count
# end
#
# def self.down
# add_column :items, :incomplete_items_count
# add_column :items, :completed_items_count
# end
# end
#
# And sometimes you need to do something in SQL not abstracted directly by migrations:
#
# class MakeJoinUnique < ActiveRecord::Migration
# def self.up
# execute "ALTER TABLE `pages_linked_pages` ADD UNIQUE `page_id_linked_page_id` (`page_id`,`linked_page_id`)"
# end
#
# def self.down
# execute "ALTER TABLE `pages_linked_pages` DROP INDEX `page_id_linked_page_id`"
# end
# end
#
# == Using a model after changing its table
#
# Sometimes you'll want to add a column in a migration and populate it immediately after. In that case, you'll need
# to make a call to Base#reset_column_information in order to ensure that the model has the latest column data from
# after the new column was added. Example:
#
# class AddPeopleSalary < ActiveRecord::Migration
# def self.up
# add_column :people, :salary, :integer
# Person.reset_column_information
# Person.find(:all).each do |p|
# p.update_attribute :salary, SalaryCalculator.compute(p)
# end
# end
# end
#
# == Controlling verbosity
#
# By default, migrations will describe the actions they are taking, writing
# them to the console as they happen, along with benchmarks describing how
# long each step took.
#
# You can quiet them down by setting ActiveRecord::Migration.verbose = false.
#
# You can also insert your own messages and benchmarks by using the +say_with_time+
# method:
#
# def self.up
# ...
# say_with_time "Updating salaries..." do
# Person.find(:all).each do |p|
# p.update_attribute :salary, SalaryCalculator.compute(p)
# end
# end
# ...
# end
#
# The phrase "Updating salaries..." would then be printed, along with the
# benchmark for the block when the block completes.
#
# == About the schema_migrations table
#
# Rails versions 2.0 and prior used to create a table called
# <tt>schema_info</tt> when using migrations. This table contained the
# version of the schema as of the last applied migration.
#
# Starting with Rails 2.1, the <tt>schema_info</tt> table is
# (automatically) replaced by the <tt>schema_migrations</tt> table, which
# contains the version numbers of all the migrations applied.
#
# As a result, it is now possible to add migration files that are numbered
# lower than the current schema version: when migrating up, those
# never-applied "interleaved" migrations will be automatically applied, and
# when migrating down, never-applied "interleaved" migrations will be skipped.
class Migration
@@verbose = true
cattr_accessor :verbose
class << self
def up_with_benchmarks #:nodoc:
migrate(:up)
end
def down_with_benchmarks #:nodoc:
migrate(:down)
end
# Execute this migration in the named direction
def migrate(direction)
return unless respond_to?(direction)
case direction
when :up then announce "migrating"
when :down then announce "reverting"
end
result = nil
time = Benchmark.measure { result = send("#{direction}_without_benchmarks") }
case direction
when :up then announce "migrated (%.4fs)" % time.real; write
when :down then announce "reverted (%.4fs)" % time.real; write
end
result
end
# Because the method added may do an alias_method, it can be invoked
# recursively. We use @ignore_new_methods as a guard to indicate whether
# it is safe for the call to proceed.
def singleton_method_added(sym) #:nodoc:
return if defined?(@ignore_new_methods) && @ignore_new_methods
begin
@ignore_new_methods = true
case sym
when :up, :down
klass = (class << self; self; end)
klass.send(:alias_method_chain, sym, "benchmarks")
end
ensure
@ignore_new_methods = false
end
end
def write(text="")
puts(text) if verbose
end
def announce(message)
text = "#{@version} #{name}: #{message}"
length = [0, 75 - text.length].max
write "== %s %s" % [text, "=" * length]
end
def say(message, subitem=false)
write "#{subitem ? " ->" : "--"} #{message}"
end
def say_with_time(message)
say(message)
result = nil
time = Benchmark.measure { result = yield }
say "%.4fs" % time.real, :subitem
say("#{result} rows", :subitem) if result.is_a?(Integer)
result
end
def suppress_messages
save, self.verbose = verbose, false
yield
ensure
self.verbose = save
end
def method_missing(method, *arguments, &block)
arg_list = arguments.map(&:inspect) * ', '
say_with_time "#{method}(#{arg_list})" do
unless arguments.empty? || method == :execute
arguments[0] = Migrator.proper_table_name(arguments.first)
end
ActiveRecord::Base.connection.send(method, *arguments, &block)
end
end
end
end
class Migrator#:nodoc:
class << self
def migrate(migrations_path, target_version = nil)
case
when target_version.nil? then up(migrations_path, target_version)
when current_version > target_version then down(migrations_path, target_version)
else up(migrations_path, target_version)
end
end
def rollback(migrations_path, steps=1)
migrator = self.new(:down, migrations_path)
start_index = migrator.migrations.index(migrator.current_migration)
return unless start_index
finish = migrator.migrations[start_index + steps]
down(migrations_path, finish ? finish.version : 0)
end
def up(migrations_path, target_version = nil)
self.new(:up, migrations_path, target_version).migrate
end
def down(migrations_path, target_version = nil)
self.new(:down, migrations_path, target_version).migrate
end
def run(direction, migrations_path, target_version)
self.new(direction, migrations_path, target_version).run
end
def schema_migrations_table_name
Base.table_name_prefix + 'schema_migrations' + Base.table_name_suffix
end
def current_version
version = Base.connection.select_values(
"SELECT version FROM #{schema_migrations_table_name}"
).map(&:to_i).max rescue nil
version || 0
end
def proper_table_name(name)
# Use the Active Record objects own table_name, or pre/suffix from ActiveRecord::Base if name is a symbol/string
name.table_name rescue "#{ActiveRecord::Base.table_name_prefix}#{name}#{ActiveRecord::Base.table_name_suffix}"
end
end
def initialize(direction, migrations_path, target_version = nil)
raise StandardError.new("This database does not yet support migrations") unless Base.connection.supports_migrations?
Base.connection.initialize_schema_migrations_table
@direction, @migrations_path, @target_version = direction, migrations_path, target_version
end
def current_version
self.class.current_version
end
def current_migration
migrations.detect { |m| m.version == current_version }
end
def run
target = migrations.detect { |m| m.version == @target_version }
raise UnknownMigrationVersionError.new(@target_version) if target.nil?
target.migrate(@direction)
end
def migrate
current = migrations.detect { |m| m.version == current_version }
target = migrations.detect { |m| m.version == @target_version }
if target.nil? && !@target_version.nil? && @target_version > 0
raise UnknownMigrationVersionError.new(@target_version)
end
start = up? ? 0 : (migrations.index(current) || 0)
finish = migrations.index(target) || migrations.size - 1
runnable = migrations[start..finish]
# skip the last migration if we're headed down, but not ALL the way down
runnable.pop if down? && !target.nil?
runnable.each do |migration|
Base.logger.info "Migrating to #{migration} (#{migration.version})"
# On our way up, we skip migrating the ones we've already migrated
# On our way down, we skip reverting the ones we've never migrated
next if up? && migrated.include?(migration.version.to_i)
if down? && !migrated.include?(migration.version.to_i)
migration.announce 'never migrated, skipping'; migration.write
else
migration.migrate(@direction)
record_version_state_after_migrating(migration.version)
end
end
end
def migrations
@migrations ||= begin
files = Dir["#{@migrations_path}/[0-9]*_*.rb"]
migrations = files.inject([]) do |klasses, file|
version, name = file.scan(/([0-9]+)_([_a-z0-9]*).rb/).first
raise IllegalMigrationNameError.new(file) unless version
version = version.to_i
if klasses.detect { |m| m.version == version }
raise DuplicateMigrationVersionError.new(version)
end
if klasses.detect { |m| m.name == name.camelize }
raise DuplicateMigrationNameError.new(name.camelize)
end
load(file)
klasses << returning(name.camelize.constantize) do |klass|
class << klass; attr_accessor :version end
klass.version = version
end
end
migrations = migrations.sort_by(&:version)
down? ? migrations.reverse : migrations
end
end
def pending_migrations
already_migrated = migrated
migrations.reject { |m| already_migrated.include?(m.version.to_i) }
end
def migrated
sm_table = self.class.schema_migrations_table_name
Base.connection.select_values("SELECT version FROM #{sm_table}").map(&:to_i).sort
end
private
def record_version_state_after_migrating(version)
sm_table = self.class.schema_migrations_table_name
if down?
Base.connection.update("DELETE FROM #{sm_table} WHERE version = '#{version}'")
else
Base.connection.insert("INSERT INTO #{sm_table} (version) VALUES ('#{version}')")
end
end
def up?
@direction == :up
end
def down?
@direction == :down
end
end
end

View file

@ -1,163 +0,0 @@
module ActiveRecord
module NamedScope
# All subclasses of ActiveRecord::Base have two named_scopes:
# * <tt>all</tt>, which is similar to a <tt>find(:all)</tt> query, and
# * <tt>scoped</tt>, which allows for the creation of anonymous scopes, on the fly: <tt>Shirt.scoped(:conditions => {:color => 'red'}).scoped(:include => :washing_instructions)</tt>
#
# These anonymous scopes tend to be useful when procedurally generating complex queries, where passing
# intermediate values (scopes) around as first-class objects is convenient.
def self.included(base)
base.class_eval do
extend ClassMethods
named_scope :scoped, lambda { |scope| scope }
end
end
module ClassMethods
def scopes
read_inheritable_attribute(:scopes) || write_inheritable_attribute(:scopes, {})
end
# Adds a class method for retrieving and querying objects. A scope represents a narrowing of a database query,
# such as <tt>:conditions => {:color => :red}, :select => 'shirts.*', :include => :washing_instructions</tt>.
#
# class Shirt < ActiveRecord::Base
# named_scope :red, :conditions => {:color => 'red'}
# named_scope :dry_clean_only, :joins => :washing_instructions, :conditions => ['washing_instructions.dry_clean_only = ?', true]
# end
#
# The above calls to <tt>named_scope</tt> define class methods <tt>Shirt.red</tt> and <tt>Shirt.dry_clean_only</tt>. <tt>Shirt.red</tt>,
# in effect, represents the query <tt>Shirt.find(:all, :conditions => {:color => 'red'})</tt>.
#
# Unlike Shirt.find(...), however, the object returned by <tt>Shirt.red</tt> is not an Array; it resembles the association object
# constructed by a <tt>has_many</tt> declaration. For instance, you can invoke <tt>Shirt.red.find(:first)</tt>, <tt>Shirt.red.count</tt>,
# <tt>Shirt.red.find(:all, :conditions => {:size => 'small'})</tt>. Also, just
# as with the association objects, name scopes acts like an Array, implementing Enumerable; <tt>Shirt.red.each(&block)</tt>,
# <tt>Shirt.red.first</tt>, and <tt>Shirt.red.inject(memo, &block)</tt> all behave as if Shirt.red really were an Array.
#
# These named scopes are composable. For instance, <tt>Shirt.red.dry_clean_only</tt> will produce all shirts that are both red and dry clean only.
# Nested finds and calculations also work with these compositions: <tt>Shirt.red.dry_clean_only.count</tt> returns the number of garments
# for which these criteria obtain. Similarly with <tt>Shirt.red.dry_clean_only.average(:thread_count)</tt>.
#
# All scopes are available as class methods on the ActiveRecord::Base descendent upon which the scopes were defined. But they are also available to
# <tt>has_many</tt> associations. If,
#
# class Person < ActiveRecord::Base
# has_many :shirts
# end
#
# then <tt>elton.shirts.red.dry_clean_only</tt> will return all of Elton's red, dry clean
# only shirts.
#
# Named scopes can also be procedural.
#
# class Shirt < ActiveRecord::Base
# named_scope :colored, lambda { |color|
# { :conditions => { :color => color } }
# }
# end
#
# In this example, <tt>Shirt.colored('puce')</tt> finds all puce shirts.
#
# Named scopes can also have extensions, just as with <tt>has_many</tt> declarations:
#
# class Shirt < ActiveRecord::Base
# named_scope :red, :conditions => {:color => 'red'} do
# def dom_id
# 'red_shirts'
# end
# end
# end
#
#
# For testing complex named scopes, you can examine the scoping options using the
# <tt>proxy_options</tt> method on the proxy itself.
#
# class Shirt < ActiveRecord::Base
# named_scope :colored, lambda { |color|
# { :conditions => { :color => color } }
# }
# end
#
# expected_options = { :conditions => { :colored => 'red' } }
# assert_equal expected_options, Shirt.colored('red').proxy_options
def named_scope(name, options = {}, &block)
scopes[name] = lambda do |parent_scope, *args|
Scope.new(parent_scope, case options
when Hash
options
when Proc
options.call(*args)
end, &block)
end
(class << self; self end).instance_eval do
define_method name do |*args|
scopes[name].call(self, *args)
end
end
end
end
class Scope
attr_reader :proxy_scope, :proxy_options
[].methods.each do |m|
unless m =~ /(^__|^nil\?|^send|^object_id$|class|extend|find|count|sum|average|maximum|minimum|paginate|first|last|empty?)/
delegate m, :to => :proxy_found
end
end
delegate :scopes, :with_scope, :to => :proxy_scope
def initialize(proxy_scope, options, &block)
[options[:extend]].flatten.each { |extension| extend extension } if options[:extend]
extend Module.new(&block) if block_given?
@proxy_scope, @proxy_options = proxy_scope, options.except(:extend)
end
def reload
load_found; self
end
def first(*args)
if args.first.kind_of?(Integer) || (@found && !args.first.kind_of?(Hash))
proxy_found.first(*args)
else
find(:first, *args)
end
end
def last(*args)
if args.first.kind_of?(Integer) || (@found && !args.first.kind_of?(Hash))
proxy_found.last(*args)
else
find(:last, *args)
end
end
def empty?
@found ? @found.empty? : count.zero?
end
protected
def proxy_found
@found || load_found
end
private
def method_missing(method, *args, &block)
if scopes.include?(method)
scopes[method].call(self, *args)
else
with_scope :find => proxy_options do
proxy_scope.send(method, *args, &block)
end
end
end
def load_found
@found = find(:all)
end
end
end
end

View file

@ -1,195 +0,0 @@
require 'singleton'
require 'set'
module ActiveRecord
module Observing # :nodoc:
def self.included(base)
base.extend ClassMethods
end
module ClassMethods
# Activates the observers assigned. Examples:
#
# # Calls PersonObserver.instance
# ActiveRecord::Base.observers = :person_observer
#
# # Calls Cacher.instance and GarbageCollector.instance
# ActiveRecord::Base.observers = :cacher, :garbage_collector
#
# # Same as above, just using explicit class references
# ActiveRecord::Base.observers = Cacher, GarbageCollector
#
# Note: Setting this does not instantiate the observers yet. +instantiate_observers+ is
# called during startup, and before each development request.
def observers=(*observers)
@observers = observers.flatten
end
# Gets the current observers.
def observers
@observers ||= []
end
# Instantiate the global Active Record observers.
def instantiate_observers
return if @observers.blank?
@observers.each do |observer|
if observer.respond_to?(:to_sym) # Symbol or String
observer.to_s.camelize.constantize.instance
elsif observer.respond_to?(:instance)
observer.instance
else
raise ArgumentError, "#{observer} must be a lowercase, underscored class name (or an instance of the class itself) responding to the instance method. Example: Person.observers = :big_brother # calls BigBrother.instance"
end
end
end
protected
# Notify observers when the observed class is subclassed.
def inherited(subclass)
super
changed
notify_observers :observed_class_inherited, subclass
end
end
end
# Observer classes respond to lifecycle callbacks to implement trigger-like
# behavior outside the original class. This is a great way to reduce the
# clutter that normally comes when the model class is burdened with
# functionality that doesn't pertain to the core responsibility of the
# class. Example:
#
# class CommentObserver < ActiveRecord::Observer
# def after_save(comment)
# Notifications.deliver_comment("admin@do.com", "New comment was posted", comment)
# end
# end
#
# This Observer sends an email when a Comment#save is finished.
#
# class ContactObserver < ActiveRecord::Observer
# def after_create(contact)
# contact.logger.info('New contact added!')
# end
#
# def after_destroy(contact)
# contact.logger.warn("Contact with an id of #{contact.id} was destroyed!")
# end
# end
#
# This Observer uses logger to log when specific callbacks are triggered.
#
# == Observing a class that can't be inferred
#
# Observers will by default be mapped to the class with which they share a name. So CommentObserver will
# be tied to observing Comment, ProductManagerObserver to ProductManager, and so on. If you want to name your observer
# differently than the class you're interested in observing, you can use the Observer.observe class method which takes
# either the concrete class (Product) or a symbol for that class (:product):
#
# class AuditObserver < ActiveRecord::Observer
# observe :account
#
# def after_update(account)
# AuditTrail.new(account, "UPDATED")
# end
# end
#
# If the audit observer needs to watch more than one kind of object, this can be specified with multiple arguments:
#
# class AuditObserver < ActiveRecord::Observer
# observe :account, :balance
#
# def after_update(record)
# AuditTrail.new(record, "UPDATED")
# end
# end
#
# The AuditObserver will now act on both updates to Account and Balance by treating them both as records.
#
# == Available callback methods
#
# The observer can implement callback methods for each of the methods described in the Callbacks module.
#
# == Storing Observers in Rails
#
# If you're using Active Record within Rails, observer classes are usually stored in app/models with the
# naming convention of app/models/audit_observer.rb.
#
# == Configuration
#
# In order to activate an observer, list it in the <tt>config.active_record.observers</tt> configuration setting in your
# <tt>config/environment.rb</tt> file.
#
# config.active_record.observers = :comment_observer, :signup_observer
#
# Observers will not be invoked unless you define these in your application configuration.
#
# == Loading
#
# Observers register themselves in the model class they observe, since it is the class that
# notifies them of events when they occur. As a side-effect, when an observer is loaded its
# corresponding model class is loaded.
#
# Up to (and including) Rails 2.0.2 observers were instantiated between plugins and
# application initializers. Now observers are loaded after application initializers,
# so observed models can make use of extensions.
#
# If by any chance you are using observed models in the initialization you can still
# load their observers by calling <tt>ModelObserver.instance</tt> before. Observers are
# singletons and that call instantiates and registers them.
#
class Observer
include Singleton
class << self
# Attaches the observer to the supplied model classes.
def observe(*models)
models.flatten!
models.collect! { |model| model.is_a?(Symbol) ? model.to_s.camelize.constantize : model }
define_method(:observed_classes) { Set.new(models) }
end
# The class observed by default is inferred from the observer's class name:
# assert_equal Person, PersonObserver.observed_class
def observed_class
if observed_class_name = name[/(.*)Observer/, 1]
observed_class_name.constantize
else
nil
end
end
end
# Start observing the declared classes and their subclasses.
def initialize
Set.new(observed_classes + observed_subclasses).each { |klass| add_observer! klass }
end
# Send observed_method(object) if the method exists.
def update(observed_method, object) #:nodoc:
send(observed_method, object) if respond_to?(observed_method)
end
# Special method sent by the observed class when it is inherited.
# Passes the new subclass.
def observed_class_inherited(subclass) #:nodoc:
self.class.observe(observed_classes + [subclass])
add_observer!(subclass)
end
protected
def observed_classes
Set.new([self.class.observed_class].compact.flatten)
end
def observed_subclasses
observed_classes.sum([]) { |klass| klass.send(:subclasses) }
end
def add_observer!(klass)
klass.add_observer(self)
klass.class_eval 'def after_find() end' unless klass.respond_to?(:after_find)
end
end
end

View file

@ -1,21 +0,0 @@
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)
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
end

View file

@ -1,239 +0,0 @@
module ActiveRecord
module Reflection # :nodoc:
def self.included(base)
base.extend(ClassMethods)
end
# Reflection allows you to interrogate Active Record classes and objects about their associations and aggregations.
# This information can, for example, be used in a form builder that took an Active Record object and created input
# fields for all of the attributes depending on their type and displayed the associations to other objects.
#
# You can find the interface for the AggregateReflection and AssociationReflection classes in the abstract MacroReflection class.
module ClassMethods
def create_reflection(macro, name, options, active_record)
case macro
when :has_many, :belongs_to, :has_one, :has_and_belongs_to_many
reflection = AssociationReflection.new(macro, name, options, active_record)
when :composed_of
reflection = AggregateReflection.new(macro, name, options, active_record)
end
write_inheritable_hash :reflections, name => reflection
reflection
end
# Returns a hash containing all AssociationReflection objects for the current class
# Example:
#
# Invoice.reflections
# Account.reflections
#
def reflections
read_inheritable_attribute(:reflections) || write_inheritable_attribute(:reflections, {})
end
# Returns an array of AggregateReflection objects for all the aggregations in the class.
def reflect_on_all_aggregations
reflections.values.select { |reflection| reflection.is_a?(AggregateReflection) }
end
# Returns the AggregateReflection object for the named +aggregation+ (use the symbol). Example:
#
# Account.reflect_on_aggregation(:balance) # returns the balance AggregateReflection
#
def reflect_on_aggregation(aggregation)
reflections[aggregation].is_a?(AggregateReflection) ? reflections[aggregation] : nil
end
# Returns an array of AssociationReflection objects for all the associations in the class. If you only want to reflect on a
# certain association type, pass in the symbol (<tt>:has_many</tt>, <tt>:has_one</tt>, <tt>:belongs_to</tt>) for that as the first parameter.
# Example:
#
# Account.reflect_on_all_associations # returns an array of all associations
# Account.reflect_on_all_associations(:has_many) # returns an array of all has_many associations
#
def reflect_on_all_associations(macro = nil)
association_reflections = reflections.values.select { |reflection| reflection.is_a?(AssociationReflection) }
macro ? association_reflections.select { |reflection| reflection.macro == macro } : association_reflections
end
# Returns the AssociationReflection object for the named +association+ (use the symbol). Example:
#
# Account.reflect_on_association(:owner) # returns the owner AssociationReflection
# Invoice.reflect_on_association(:line_items).macro # returns :has_many
#
def reflect_on_association(association)
reflections[association].is_a?(AssociationReflection) ? reflections[association] : nil
end
end
# Abstract base class for AggregateReflection and AssociationReflection that describes the interface available for both of
# those classes. Objects of AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods.
class MacroReflection
attr_reader :active_record
def initialize(macro, name, options, active_record)
@macro, @name, @options, @active_record = macro, name, options, active_record
end
# Returns the name of the macro. For example, <tt>composed_of :balance, :class_name => 'Money'</tt> will return
# <tt>:balance</tt> or for <tt>has_many :clients</tt> it will return <tt>:clients</tt>.
def name
@name
end
# Returns the macro type. For example, <tt>composed_of :balance, :class_name => 'Money'</tt> will return <tt>:composed_of</tt>
# or for <tt>has_many :clients</tt> will return <tt>:has_many</tt>.
def macro
@macro
end
# Returns the hash of options used for the macro. For example, it would return <tt>{ :class_name => "Money" }</tt> for
# <tt>composed_of :balance, :class_name => 'Money'</tt> or +{}+ for <tt>has_many :clients</tt>.
def options
@options
end
# Returns the class for the macro. For example, <tt>composed_of :balance, :class_name => 'Money'</tt> returns the Money
# class and <tt>has_many :clients</tt> returns the Client class.
def klass
@klass ||= class_name.constantize
end
# Returns the class name for the macro. For example, <tt>composed_of :balance, :class_name => 'Money'</tt> returns <tt>'Money'</tt>
# and <tt>has_many :clients</tt> returns <tt>'Client'</tt>.
def class_name
@class_name ||= options[:class_name] || derive_class_name
end
# Returns +true+ if +self+ and +other_aggregation+ have the same +name+ attribute, +active_record+ attribute,
# and +other_aggregation+ has an options hash assigned to it.
def ==(other_aggregation)
name == other_aggregation.name && other_aggregation.options && active_record == other_aggregation.active_record
end
private
def derive_class_name
name.to_s.camelize
end
end
# Holds all the meta-data about an aggregation as it was specified in the Active Record class.
class AggregateReflection < MacroReflection #:nodoc:
end
# Holds all the meta-data about an association as it was specified in the Active Record class.
class AssociationReflection < MacroReflection #:nodoc:
def klass
@klass ||= active_record.send(:compute_type, class_name)
end
def table_name
@table_name ||= klass.table_name
end
def quoted_table_name
@quoted_table_name ||= klass.quoted_table_name
end
def primary_key_name
@primary_key_name ||= options[:foreign_key] || derive_primary_key_name
end
def association_foreign_key
@association_foreign_key ||= @options[:association_foreign_key] || class_name.foreign_key
end
def counter_cache_column
if options[:counter_cache] == true
"#{active_record.name.underscore.pluralize}_count"
elsif options[:counter_cache]
options[:counter_cache]
end
end
# Returns the AssociationReflection object specified in the <tt>:through</tt> option
# of a HasManyThrough or HasOneThrough association. Example:
#
# class Post < ActiveRecord::Base
# has_many :taggings
# has_many :tags, :through => :taggings
# end
#
# tags_reflection = Post.reflect_on_association(:tags)
# taggings_reflection = tags_reflection.through_reflection
#
def through_reflection
@through_reflection ||= options[:through] ? active_record.reflect_on_association(options[:through]) : false
end
# Gets an array of possible <tt>:through</tt> source reflection names:
#
# [:singularized, :pluralized]
#
def source_reflection_names
@source_reflection_names ||= (options[:source] ? [options[:source]] : [name.to_s.singularize, name]).collect { |n| n.to_sym }
end
# Gets the source of the through reflection. It checks both a singularized and pluralized form for <tt>:belongs_to</tt> or <tt>:has_many</tt>.
# (The <tt>:tags</tt> association on Tagging below.)
#
# class Post < ActiveRecord::Base
# has_many :taggings
# has_many :tags, :through => :taggings
# end
#
def source_reflection
return nil unless through_reflection
@source_reflection ||= source_reflection_names.collect { |name| through_reflection.klass.reflect_on_association(name) }.compact.first
end
def check_validity!
if options[:through]
if through_reflection.nil?
raise HasManyThroughAssociationNotFoundError.new(active_record.name, self)
end
if source_reflection.nil?
raise HasManyThroughSourceAssociationNotFoundError.new(self)
end
if options[:source_type] && source_reflection.options[:polymorphic].nil?
raise HasManyThroughAssociationPointlessSourceTypeError.new(active_record.name, self, source_reflection)
end
if source_reflection.options[:polymorphic] && options[:source_type].nil?
raise HasManyThroughAssociationPolymorphicError.new(active_record.name, self, source_reflection)
end
unless [:belongs_to, :has_many].include?(source_reflection.macro) && source_reflection.options[:through].nil?
raise HasManyThroughSourceAssociationMacroError.new(self)
end
end
end
private
def derive_class_name
# get the class_name of the belongs_to association of the through reflection
if through_reflection
options[:source_type] || source_reflection.class_name
else
class_name = name.to_s.camelize
class_name = class_name.singularize if [ :has_many, :has_and_belongs_to_many ].include?(macro)
class_name
end
end
def derive_primary_key_name
if macro == :belongs_to
"#{name}_id"
elsif options[:as]
"#{options[:as]}_id"
else
active_record.name.foreign_key
end
end
end
end
end

View file

@ -1,51 +0,0 @@
module ActiveRecord
# Allows programmers to programmatically define a schema in a portable
# DSL. This means you can define tables, indexes, etc. without using SQL
# directly, so your applications can more easily support multiple
# databases.
#
# Usage:
#
# ActiveRecord::Schema.define do
# create_table :authors do |t|
# t.string :name, :null => false
# end
#
# add_index :authors, :name, :unique
#
# create_table :posts do |t|
# t.integer :author_id, :null => false
# t.string :subject
# t.text :body
# t.boolean :private, :default => false
# end
#
# add_index :posts, :author_id
# end
#
# ActiveRecord::Schema is only supported by database adapters that also
# support migrations, the two features being very similar.
class Schema < Migration
private_class_method :new
# Eval the given block. All methods available to the current connection
# adapter are available within the block, so you can easily use the
# database definition DSL to build up your schema (+create_table+,
# +add_index+, etc.).
#
# The +info+ hash is optional, and if given is used to define metadata
# about the current schema (currently, only the schema's version):
#
# ActiveRecord::Schema.define(:version => 20380119000001) do
# ...
# end
def self.define(info={}, &block)
instance_eval(&block)
unless info[:version].blank?
initialize_schema_migrations_table
assume_migrated_upto_version info[:version]
end
end
end
end

View file

@ -1,171 +0,0 @@
require 'stringio'
require 'bigdecimal'
module ActiveRecord
# This class is used to dump the database schema for some connection to some
# output format (i.e., ActiveRecord::Schema).
class SchemaDumper #:nodoc:
private_class_method :new
# 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
cattr_accessor :ignore_tables
@@ignore_tables = []
def self.dump(connection=ActiveRecord::Base.connection, stream=STDOUT)
new(connection).dump(stream)
stream
end
def dump(stream)
header(stream)
tables(stream)
trailer(stream)
stream
end
private
def initialize(connection)
@connection = connection
@types = @connection.native_database_types
@version = Migrator::current_version rescue nil
end
def header(stream)
define_params = @version ? ":version => #{@version}" : ""
stream.puts <<HEADER
# This file is auto-generated from the current state of the database. Instead of editing this file,
# please use the migrations feature of Active Record to incrementally modify your database, and
# then regenerate this schema definition.
#
# Note that this schema.rb definition is the authoritative source for your database schema. If you need
# to create the application database on another system, you should be using db:schema:load, not running
# all the migrations from scratch. The latter is a flawed and unsustainable approach (the more migrations
# you'll amass, the slower it'll run and the greater likelihood for issues).
#
# It's strongly recommended to check this file into your version control system.
ActiveRecord::Schema.define(#{define_params}) do
HEADER
end
def trailer(stream)
stream.puts "end"
end
def tables(stream)
@connection.tables.sort.each do |tbl|
next if ['schema_migrations', ignore_tables].flatten.any? do |ignored|
case ignored
when String; tbl == ignored
when Regexp; tbl =~ ignored
else
raise StandardError, 'ActiveRecord::SchemaDumper.ignore_tables accepts an array of String and / or Regexp values.'
end
end
table(tbl, stream)
end
end
def table(table, stream)
columns = @connection.columns(table)
begin
tbl = StringIO.new
if @connection.respond_to?(:pk_and_sequence_for)
pk, pk_seq = @connection.pk_and_sequence_for(table)
end
pk ||= 'id'
tbl.print " create_table #{table.inspect}"
if columns.detect { |c| c.name == pk }
if pk != 'id'
tbl.print %Q(, :primary_key => "#{pk}")
end
else
tbl.print ", :id => false"
end
tbl.print ", :force => true"
tbl.puts " do |t|"
column_specs = columns.map do |column|
raise StandardError, "Unknown type '#{column.sql_type}' for column '#{column.name}'" if @types[column.type].nil?
next if column.name == pk
spec = {}
spec[:name] = column.name.inspect
spec[:type] = column.type.to_s
spec[:limit] = column.limit.inspect if column.limit != @types[column.type][:limit] && column.type != :decimal
spec[:precision] = column.precision.inspect if !column.precision.nil?
spec[:scale] = column.scale.inspect if !column.scale.nil?
spec[:null] = 'false' if !column.null
spec[:default] = default_string(column.default) if !column.default.nil?
(spec.keys - [:name, :type]).each{ |k| spec[k].insert(0, "#{k.inspect} => ")}
spec
end.compact
# find all migration keys used in this table
keys = [:name, :limit, :precision, :scale, :default, :null] & column_specs.map(&:keys).flatten
# figure out the lengths for each column based on above keys
lengths = keys.map{ |key| column_specs.map{ |spec| spec[key] ? spec[key].length + 2 : 0 }.max }
# the string we're going to sprintf our values against, with standardized column widths
format_string = lengths.map{ |len| "%-#{len}s" }
# find the max length for the 'type' column, which is special
type_length = column_specs.map{ |column| column[:type].length }.max
# add column type definition to our format string
format_string.unshift " t.%-#{type_length}s "
format_string *= ''
column_specs.each do |colspec|
values = keys.zip(lengths).map{ |key, len| colspec.key?(key) ? colspec[key] + ", " : " " * len }
values.unshift colspec[:type]
tbl.print((format_string % values).gsub(/,\s*$/, ''))
tbl.puts
end
tbl.puts " end"
tbl.puts
indexes(table, tbl)
tbl.rewind
stream.print tbl.read
rescue => e
stream.puts "# Could not dump table #{table.inspect} because of following #{e.class}"
stream.puts "# #{e.message}"
stream.puts
end
stream
end
def default_string(value)
case value
when BigDecimal
value.to_s
when Date, DateTime, Time
"'" + value.to_s(:db) + "'"
else
value.inspect
end
end
def indexes(table, stream)
indexes = @connection.indexes(table)
indexes.each do |index|
stream.print " add_index #{index.table.inspect}, #{index.columns.inspect}, :name => #{index.name.inspect}"
stream.print ", :unique => true" if index.unique
stream.puts
end
stream.puts unless indexes.empty?
end
end
end

View file

@ -1,98 +0,0 @@
module ActiveRecord #:nodoc:
module Serialization
class Serializer #:nodoc:
attr_reader :options
def initialize(record, options = {})
@record, @options = record, options.dup
end
# To replicate the behavior in ActiveRecord#attributes,
# <tt>:except</tt> takes precedence over <tt>:only</tt>. If <tt>:only</tt> is not set
# for a N level model but is set for the N+1 level models,
# then because <tt>:except</tt> is set to a default value, the second
# level model can have both <tt>:except</tt> and <tt>:only</tt> set. So if
# <tt>:only</tt> is set, always delete <tt>:except</tt>.
def serializable_attribute_names
attribute_names = @record.attribute_names
if options[:only]
options.delete(:except)
attribute_names = attribute_names & Array(options[:only]).collect { |n| n.to_s }
else
options[:except] = Array(options[:except]) | Array(@record.class.inheritance_column)
attribute_names = attribute_names - options[:except].collect { |n| n.to_s }
end
attribute_names
end
def serializable_method_names
Array(options[:methods]).inject([]) do |method_attributes, name|
method_attributes << name if @record.respond_to?(name.to_s)
method_attributes
end
end
def serializable_names
serializable_attribute_names + serializable_method_names
end
# Add associations specified via the <tt>:includes</tt> option.
# Expects a block that takes as arguments:
# +association+ - name of the association
# +records+ - the association record(s) to be serialized
# +opts+ - options for the association records
def add_includes(&block)
if include_associations = options.delete(:include)
base_only_or_except = { :except => options[:except],
:only => options[:only] }
include_has_options = include_associations.is_a?(Hash)
associations = include_has_options ? include_associations.keys : Array(include_associations)
for association in associations
records = case @record.class.reflect_on_association(association).macro
when :has_many, :has_and_belongs_to_many
@record.send(association).to_a
when :has_one, :belongs_to
@record.send(association)
end
unless records.nil?
association_options = include_has_options ? include_associations[association] : base_only_or_except
opts = options.merge(association_options)
yield(association, records, opts)
end
end
options[:include] = include_associations
end
end
def serializable_record
returning(serializable_record = {}) do
serializable_names.each { |name| serializable_record[name] = @record.send(name) }
add_includes do |association, records, opts|
if records.is_a?(Enumerable)
serializable_record[association] = records.collect { |r| self.class.new(r, opts).serializable_record }
else
serializable_record[association] = self.class.new(records, opts).serializable_record
end
end
end
end
def serialize
# overwrite to implement
end
def to_s(&block)
serialize(&block)
end
end
end
end
require 'active_record/serializers/xml_serializer'
require 'active_record/serializers/json_serializer'

View file

@ -1,80 +0,0 @@
module ActiveRecord #:nodoc:
module Serialization
def self.included(base)
base.cattr_accessor :include_root_in_json, :instance_writer => false
base.extend ClassMethods
end
# Returns a JSON string representing the model. Some configuration is
# available through +options+.
#
# Without any +options+, the returned JSON string will include all
# the model's attributes. For example:
#
# konata = User.find(1)
# konata.to_json
# # => {"id": 1, "name": "Konata Izumi", "age": 16,
# "created_at": "2006/08/01", "awesome": true}
#
# The <tt>:only</tt> and <tt>:except</tt> options can be used to limit the attributes
# included, and work similar to the +attributes+ method. For example:
#
# konata.to_json(:only => [ :id, :name ])
# # => {"id": 1, "name": "Konata Izumi"}
#
# konata.to_json(:except => [ :id, :created_at, :age ])
# # => {"name": "Konata Izumi", "awesome": true}
#
# To include any methods on the model, use <tt>:methods</tt>.
#
# konata.to_json(:methods => :permalink)
# # => {"id": 1, "name": "Konata Izumi", "age": 16,
# "created_at": "2006/08/01", "awesome": true,
# "permalink": "1-konata-izumi"}
#
# To include associations, use <tt>:include</tt>.
#
# konata.to_json(:include => :posts)
# # => {"id": 1, "name": "Konata Izumi", "age": 16,
# "created_at": "2006/08/01", "awesome": true,
# "posts": [{"id": 1, "author_id": 1, "title": "Welcome to the weblog"},
# {"id": 2, author_id: 1, "title": "So I was thinking"}]}
#
# 2nd level and higher order associations work as well:
#
# konata.to_json(:include => { :posts => {
# :include => { :comments => {
# :only => :body } },
# :only => :title } })
# # => {"id": 1, "name": "Konata Izumi", "age": 16,
# "created_at": "2006/08/01", "awesome": true,
# "posts": [{"comments": [{"body": "1st post!"}, {"body": "Second!"}],
# "title": "Welcome to the weblog"},
# {"comments": [{"body": "Don't think too hard"}],
# "title": "So I was thinking"}]}
def to_json(options = {})
if include_root_in_json
"{#{self.class.json_class_name}: #{JsonSerializer.new(self, options).to_s}}"
else
JsonSerializer.new(self, options).to_s
end
end
def from_json(json)
self.attributes = ActiveSupport::JSON.decode(json)
self
end
class JsonSerializer < ActiveRecord::Serialization::Serializer #:nodoc:
def serialize
serializable_record.to_json
end
end
module ClassMethods
def json_class_name
@json_class_name ||= name.demodulize.underscore.inspect
end
end
end
end

View file

@ -1,338 +0,0 @@
module ActiveRecord #:nodoc:
module Serialization
# Builds an XML document to represent the model. Some configuration is
# available through +options+. However more complicated cases should
# override ActiveRecord::Base#to_xml.
#
# By default the generated XML document will include the processing
# instruction and all the object's attributes. For example:
#
# <?xml version="1.0" encoding="UTF-8"?>
# <topic>
# <title>The First Topic</title>
# <author-name>David</author-name>
# <id type="integer">1</id>
# <approved type="boolean">false</approved>
# <replies-count type="integer">0</replies-count>
# <bonus-time type="datetime">2000-01-01T08:28:00+12:00</bonus-time>
# <written-on type="datetime">2003-07-16T09:28:00+1200</written-on>
# <content>Have a nice day</content>
# <author-email-address>david@loudthinking.com</author-email-address>
# <parent-id></parent-id>
# <last-read type="date">2004-04-15</last-read>
# </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>.
# 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+.
#
# For instance:
#
# topic.to_xml(:skip_instruct => true, :except => [ :id, :bonus_time, :written_on, :replies_count ])
#
# <topic>
# <title>The First Topic</title>
# <author-name>David</author-name>
# <approved type="boolean">false</approved>
# <content>Have a nice day</content>
# <author-email-address>david@loudthinking.com</author-email-address>
# <parent-id></parent-id>
# <last-read type="date">2004-04-15</last-read>
# </topic>
#
# To include first level associations use <tt>:include</tt>:
#
# firm.to_xml :include => [ :account, :clients ]
#
# <?xml version="1.0" encoding="UTF-8"?>
# <firm>
# <id type="integer">1</id>
# <rating type="integer">1</rating>
# <name>37signals</name>
# <clients type="array">
# <client>
# <rating type="integer">1</rating>
# <name>Summit</name>
# </client>
# <client>
# <rating type="integer">1</rating>
# <name>Microsoft</name>
# </client>
# </clients>
# <account>
# <id type="integer">1</id>
# <credit-limit type="integer">50</credit-limit>
# </account>
# </firm>
#
# To include deeper levels of associations pass a hash like this:
#
# firm.to_xml :include => {:account => {}, :clients => {:include => :address}}
# <?xml version="1.0" encoding="UTF-8"?>
# <firm>
# <id type="integer">1</id>
# <rating type="integer">1</rating>
# <name>37signals</name>
# <clients type="array">
# <client>
# <rating type="integer">1</rating>
# <name>Summit</name>
# <address>
# ...
# </address>
# </client>
# <client>
# <rating type="integer">1</rating>
# <name>Microsoft</name>
# <address>
# ...
# </address>
# </client>
# </clients>
# <account>
# <id type="integer">1</id>
# <credit-limit type="integer">50</credit-limit>
# </account>
# </firm>
#
# To include any methods on the model being called use <tt>:methods</tt>:
#
# firm.to_xml :methods => [ :calculated_earnings, :real_earnings ]
#
# <firm>
# # ... normal attributes as shown above ...
# <calculated-earnings>100000000000000000</calculated-earnings>
# <real-earnings>5</real-earnings>
# </firm>
#
# To call any additional Procs use <tt>:procs</tt>. The Procs are passed a
# modified version of the options hash that was given to +to_xml+:
#
# proc = Proc.new { |options| options[:builder].tag!('abc', 'def') }
# firm.to_xml :procs => [ proc ]
#
# <firm>
# # ... normal attributes as shown above ...
# <abc>def</abc>
# </firm>
#
# Alternatively, you can yield the builder object as part of the +to_xml+ call:
#
# firm.to_xml do |xml|
# xml.creator do
# xml.first_name "David"
# xml.last_name "Heinemeier Hansson"
# end
# end
#
# <firm>
# # ... normal attributes as shown above ...
# <creator>
# <first_name>David</first_name>
# <last_name>Heinemeier Hansson</last_name>
# </creator>
# </firm>
#
# As noted above, you may override +to_xml+ in your ActiveRecord::Base
# subclasses to have complete control about what's generated. The general
# form of doing this is:
#
# class IHaveMyOwnXML < ActiveRecord::Base
# def to_xml(options = {})
# options[:indent] ||= 2
# xml = options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent])
# xml.instruct! unless options[:skip_instruct]
# xml.level_one do
# xml.tag!(:second_level, 'content')
# end
# end
# end
def to_xml(options = {}, &block)
serializer = XmlSerializer.new(self, options)
block_given? ? serializer.to_s(&block) : serializer.to_s
end
def from_xml(xml)
self.attributes = Hash.from_xml(xml).values.first
self
end
end
class XmlSerializer < ActiveRecord::Serialization::Serializer #:nodoc:
def builder
@builder ||= begin
options[:indent] ||= 2
builder = options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent])
unless options[:skip_instruct]
builder.instruct!
options[:skip_instruct] = true
end
builder
end
end
def root
root = (options[:root] || @record.class.to_s.underscore).to_s
dasherize? ? root.dasherize : root
end
def dasherize?
!options.has_key?(:dasherize) || options[:dasherize]
end
def serializable_attributes
serializable_attribute_names.collect { |name| Attribute.new(name, @record) }
end
def serializable_method_attributes
Array(options[:methods]).inject([]) do |method_attributes, name|
method_attributes << MethodAttribute.new(name.to_s, @record) if @record.respond_to?(name.to_s)
method_attributes
end
end
def add_attributes
(serializable_attributes + serializable_method_attributes).each do |attribute|
add_tag(attribute)
end
end
def add_procs
if procs = options.delete(:procs)
[ *procs ].each do |proc|
proc.call(options)
end
end
end
def add_tag(attribute)
builder.tag!(
dasherize? ? attribute.name.dasherize : attribute.name,
attribute.value.to_s,
attribute.decorations(!options[:skip_types])
)
end
def add_associations(association, records, opts)
if records.is_a?(Enumerable)
tag = association.to_s
tag = tag.dasherize if dasherize?
if records.empty?
builder.tag!(tag, :type => :array)
else
builder.tag!(tag, :type => :array) do
association_name = association.to_s.singularize
records.each do |record|
record.to_xml opts.merge(
:root => association_name,
:type => (record.class.to_s.underscore == association_name ? nil : record.class.name)
)
end
end
end
else
if record = @record.send(association)
record.to_xml(opts.merge(:root => association))
end
end
end
def serialize
args = [root]
if options[:namespace]
args << {:xmlns=>options[:namespace]}
end
if options[:type]
args << {:type=>options[:type]}
end
builder.tag!(*args) do
add_attributes
procs = options.delete(:procs)
add_includes { |association, records, opts| add_associations(association, records, opts) }
options[:procs] = procs
add_procs
yield builder if block_given?
end
end
class Attribute #:nodoc:
attr_reader :name, :value, :type
def initialize(name, record)
@name, @record = name, record
@type = compute_type
@value = compute_value
end
# There is a significant speed improvement if the value
# does not need to be escaped, as <tt>tag!</tt> escapes all values
# to ensure that valid XML is generated. For known binary
# values, it is at least an order of magnitude faster to
# Base64 encode binary values and directly put them in the
# output XML than to pass the original value or the Base64
# encoded value to the <tt>tag!</tt> method. It definitely makes
# no sense to Base64 encode the value and then give it to
# <tt>tag!</tt>, since that just adds additional overhead.
def needs_encoding?
![ :binary, :date, :datetime, :boolean, :float, :integer ].include?(type)
end
def decorations(include_types = true)
decorations = {}
if type == :binary
decorations[:encoding] = 'base64'
end
if include_types && type != :string
decorations[:type] = type
end
if value.nil?
decorations[:nil] = true
end
decorations
end
protected
def compute_type
type = @record.class.serialized_attributes.has_key?(name) ? :yaml : @record.class.columns_hash[name].type
case type
when :text
:string
when :time
:datetime
else
type
end
end
def compute_value
value = @record.send(name)
if formatter = Hash::XML_FORMATTING[type.to_s]
value ? formatter.call(value) : nil
else
value
end
end
end
class MethodAttribute < Attribute #:nodoc:
protected
def compute_type
Hash::XML_TYPE_NAMES[@record.send(name).class.name] || :string
end
end
end
end

View file

@ -1,36 +0,0 @@
require "active_support/test_case"
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)
# SQL Server doesn't have a separate column type just for dates,
# so the time is in the string and incorrectly formatted
if current_adapter?(:SQLServerAdapter)
assert_equal expected.strftime("%Y/%m/%d 00:00:00"), actual.strftime("%Y/%m/%d 00:00:00")
elsif current_adapter?(:SybaseAdapter)
assert_equal expected.to_s, actual.to_date.to_s, message
else
assert_equal expected.to_s, actual.to_s, message
end
end
def assert_queries(num = 1)
$query_count = 0
yield
ensure
assert_equal num, $query_count, "#{$query_count} instead of #{num} queries were executed."
end
def assert_no_queries(&block)
assert_queries(0, &block)
end
end
end

View file

@ -1,41 +0,0 @@
module ActiveRecord
# Active Record automatically timestamps create and update operations if the table has fields
# named created_at/created_on or updated_at/updated_on.
#
# Timestamping can be turned off by setting
# <tt>ActiveRecord::Base.record_timestamps = false</tt>
#
# Timestamps are in the local timezone by default but you can use UTC by setting
# <tt>ActiveRecord::Base.default_timezone = :utc</tt>
module Timestamp
def self.included(base) #:nodoc:
base.alias_method_chain :create, :timestamps
base.alias_method_chain :update, :timestamps
base.class_inheritable_accessor :record_timestamps, :instance_writer => false
base.record_timestamps = true
end
private
def create_with_timestamps #:nodoc:
if record_timestamps
t = self.class.default_timezone == :utc ? Time.now.utc : Time.now
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)
end
create_without_timestamps
end
def update_with_timestamps(*args) #:nodoc:
if record_timestamps && (!partial_updates? || changed?)
t = self.class.default_timezone == :utc ? Time.now.utc : Time.now
write_attribute('updated_at', t) if respond_to?(:updated_at)
write_attribute('updated_on', t) if respond_to?(:updated_on)
end
update_without_timestamps(*args)
end
end
end

View file

@ -1,130 +0,0 @@
require 'thread'
module ActiveRecord
module Transactions # :nodoc:
class TransactionError < ActiveRecordError # :nodoc:
end
def self.included(base)
base.extend(ClassMethods)
base.class_eval do
[:destroy, :save, :save!].each do |method|
alias_method_chain method, :transactions
end
end
end
# Transactions are protective blocks where SQL statements are only permanent if they can all succeed as one atomic action.
# The classic example is a transfer between two accounts where you can only have a deposit if the withdrawal succeeded and
# vice versa. Transactions enforce the integrity of the database and guard the data against program errors or database break-downs.
# So basically you should use transaction blocks whenever you have a number of statements that must be executed together or
# not at all. Example:
#
# transaction do
# david.withdrawal(100)
# mary.deposit(100)
# end
#
# This example will only take money from David and give to Mary if neither +withdrawal+ nor +deposit+ raises an exception.
# Exceptions will force a ROLLBACK that returns the database to the state before the transaction was begun. Be aware, though,
# that the objects will _not_ have their instance data returned to their pre-transactional state.
#
# == Different Active Record classes in a single transaction
#
# Though the transaction class method is called on some Active Record class,
# the objects within the transaction block need not all be instances of
# that class.
# In this example a <tt>Balance</tt> record is transactionally saved even
# though <tt>transaction</tt> is called on the <tt>Account</tt> class:
#
# Account.transaction do
# balance.save!
# account.save!
# end
#
# == Transactions are not distributed across database connections
#
# A transaction acts on a single database connection. If you have
# multiple class-specific databases, the transaction will not protect
# interaction among them. One workaround is to begin a transaction
# on each class whose models you alter:
#
# Student.transaction do
# Course.transaction do
# course.enroll(student)
# student.units += course.units
# end
# end
#
# This is a poor solution, but full distributed transactions are beyond
# the scope of Active Record.
#
# == Save and destroy are automatically wrapped in a transaction
#
# Both Base#save and Base#destroy come wrapped in a transaction that ensures that whatever you do in validations or callbacks
# will happen under the protected cover of a transaction. So you can use validations to check for values that the transaction
# depends on or you can raise exceptions in the callbacks to rollback.
#
# == Exception handling
#
# Also have in mind that exceptions thrown within a transaction block will be propagated (after triggering the ROLLBACK), so you
# should be ready to catch those in your application code. One exception is the ActiveRecord::Rollback exception, which will
# trigger a ROLLBACK when raised, but not be re-raised by the transaction block.
module ClassMethods
def transaction(&block)
increment_open_transactions
begin
connection.transaction(Thread.current['start_db_transaction'], &block)
ensure
decrement_open_transactions
end
end
private
def increment_open_transactions #:nodoc:
open = Thread.current['open_transactions'] ||= 0
Thread.current['start_db_transaction'] = open.zero?
Thread.current['open_transactions'] = open + 1
end
def decrement_open_transactions #:nodoc:
Thread.current['open_transactions'] -= 1
end
end
def transaction(&block)
self.class.transaction(&block)
end
def destroy_with_transactions #:nodoc:
transaction { destroy_without_transactions }
end
def save_with_transactions(perform_validation = true) #:nodoc:
rollback_active_record_state! { transaction { save_without_transactions(perform_validation) } }
end
def save_with_transactions! #:nodoc:
rollback_active_record_state! { transaction { save_without_transactions! } }
end
# Reset id and @new_record if the transaction rolls back.
def rollback_active_record_state!
id_present = has_attribute?(self.class.primary_key)
previous_id = id
previous_new_record = new_record?
yield
rescue Exception
@new_record = previous_new_record
if id_present
self.id = previous_id
else
@attributes.delete(self.class.primary_key)
@attributes_cache.delete(self.class.primary_key)
end
raise
end
end
end

View file

@ -1,961 +0,0 @@
module ActiveRecord
# Raised by save! and create! when the record is invalid. Use the
# +record+ method to retrieve the record which did not validate.
# begin
# complex_operation_that_calls_save!_internally
# rescue ActiveRecord::RecordInvalid => invalid
# puts invalid.record.errors
# end
class RecordInvalid < ActiveRecordError
attr_reader :record
def initialize(record)
@record = record
super("Validation failed: #{@record.errors.full_messages.join(", ")}")
end
end
# Active Record validation is reported to and from this object, which is used by Base#save to
# determine whether the object is in a valid state to be saved. See usage example in Validations.
class Errors
include Enumerable
def initialize(base) # :nodoc:
@base, @errors = base, {}
end
@@default_error_messages = {
:inclusion => "is not included in the list",
:exclusion => "is reserved",
:invalid => "is invalid",
:confirmation => "doesn't match confirmation",
:accepted => "must be accepted",
:empty => "can't be empty",
:blank => "can't be blank",
:too_long => "is too long (maximum is %d characters)",
:too_short => "is too short (minimum is %d characters)",
:wrong_length => "is the wrong length (should be %d characters)",
:taken => "has already been taken",
:not_a_number => "is not a number",
:greater_than => "must be greater than %d",
:greater_than_or_equal_to => "must be greater than or equal to %d",
:equal_to => "must be equal to %d",
:less_than => "must be less than %d",
:less_than_or_equal_to => "must be less than or equal to %d",
:odd => "must be odd",
:even => "must be even"
}
# Holds a hash with all the default error messages that can be replaced by your own copy or localizations.
cattr_accessor :default_error_messages
# Adds an error to the base object instead of any particular attribute. This is used
# to report errors that don't tie to any specific attribute, but rather to the object
# as a whole. These error messages don't get prepended with any field name when iterating
# with each_full, so they should be complete sentences.
def add_to_base(msg)
add(:base, msg)
end
# Adds an error message (+msg+) to the +attribute+, which will be returned on a call to <tt>on(attribute)</tt>
# for the same attribute and ensure that this error object returns false when asked if <tt>empty?</tt>. More than one
# error can be added to the same +attribute+ in which case an array will be returned on a call to <tt>on(attribute)</tt>.
# If no +msg+ is supplied, "invalid" is assumed.
def add(attribute, msg = @@default_error_messages[:invalid])
@errors[attribute.to_s] = [] if @errors[attribute.to_s].nil?
@errors[attribute.to_s] << msg
end
# Will add an error message to each of the attributes in +attributes+ that is empty.
def add_on_empty(attributes, msg = @@default_error_messages[:empty])
for attr in [attributes].flatten
value = @base.respond_to?(attr.to_s) ? @base.send(attr.to_s) : @base[attr.to_s]
is_empty = value.respond_to?("empty?") ? value.empty? : false
add(attr, msg) unless !value.nil? && !is_empty
end
end
# Will add an error message to each of the attributes in +attributes+ that is blank (using Object#blank?).
def add_on_blank(attributes, msg = @@default_error_messages[:blank])
for attr in [attributes].flatten
value = @base.respond_to?(attr.to_s) ? @base.send(attr.to_s) : @base[attr.to_s]
add(attr, msg) if value.blank?
end
end
# Returns true if the specified +attribute+ has errors associated with it.
#
# class Company < ActiveRecord::Base
# validates_presence_of :name, :address, :email
# validates_length_of :name, :in => 5..30
# end
#
# company = Company.create(:address => '123 First St.')
# company.errors.invalid?(:name) # => true
# company.errors.invalid?(:address) # => false
def invalid?(attribute)
!@errors[attribute.to_s].nil?
end
# Returns nil, if no errors are associated with the specified +attribute+.
# Returns the error message, if one error is associated with the specified +attribute+.
# Returns an array of error messages, if more than one error is associated with the specified +attribute+.
#
# class Company < ActiveRecord::Base
# validates_presence_of :name, :address, :email
# validates_length_of :name, :in => 5..30
# end
#
# company = Company.create(:address => '123 First St.')
# company.errors.on(:name) # => ["is too short (minimum is 5 characters)", "can't be blank"]
# company.errors.on(:email) # => "can't be blank"
# company.errors.on(:address) # => nil
def on(attribute)
errors = @errors[attribute.to_s]
return nil if errors.nil?
errors.size == 1 ? errors.first : errors
end
alias :[] :on
# Returns errors assigned to the base object through add_to_base according to the normal rules of on(attribute).
def on_base
on(:base)
end
# Yields each attribute and associated message per error added.
#
# class Company < ActiveRecord::Base
# validates_presence_of :name, :address, :email
# validates_length_of :name, :in => 5..30
# end
#
# company = Company.create(:address => '123 First St.')
# company.errors.each{|attr,msg| puts "#{attr} - #{msg}" } # =>
# name - is too short (minimum is 5 characters)
# name - can't be blank
# address - can't be blank
def each
@errors.each_key { |attr| @errors[attr].each { |msg| yield attr, msg } }
end
# Yields each full error message added. So Person.errors.add("first_name", "can't be empty") will be returned
# through iteration as "First name can't be empty".
#
# class Company < ActiveRecord::Base
# validates_presence_of :name, :address, :email
# validates_length_of :name, :in => 5..30
# end
#
# company = Company.create(:address => '123 First St.')
# company.errors.each_full{|msg| puts msg } # =>
# Name is too short (minimum is 5 characters)
# Name can't be blank
# Address can't be blank
def each_full
full_messages.each { |msg| yield msg }
end
# Returns all the full error messages in an array.
#
# class Company < ActiveRecord::Base
# validates_presence_of :name, :address, :email
# validates_length_of :name, :in => 5..30
# end
#
# company = Company.create(:address => '123 First St.')
# company.errors.full_messages # =>
# ["Name is too short (minimum is 5 characters)", "Name can't be blank", "Address can't be blank"]
def full_messages
full_messages = []
@errors.each_key do |attr|
@errors[attr].each do |msg|
next if msg.nil?
if attr == "base"
full_messages << msg
else
full_messages << @base.class.human_attribute_name(attr) + " " + msg
end
end
end
full_messages
end
# Returns true if no errors have been added.
def empty?
@errors.empty?
end
# Removes all errors that have been added.
def clear
@errors = {}
end
# Returns the total number of errors added. Two errors added to the same attribute will be counted as such.
def size
@errors.values.inject(0) { |error_count, attribute| error_count + attribute.size }
end
alias_method :count, :size
alias_method :length, :size
# Returns an XML representation of this error object.
#
# class Company < ActiveRecord::Base
# validates_presence_of :name, :address, :email
# validates_length_of :name, :in => 5..30
# end
#
# company = Company.create(:address => '123 First St.')
# company.errors.to_xml # =>
# <?xml version="1.0" encoding="UTF-8"?>
# <errors>
# <error>Name is too short (minimum is 5 characters)</error>
# <error>Name can't be blank</error>
# <error>Address can't be blank</error>
# </errors>
def to_xml(options={})
options[:root] ||= "errors"
options[:indent] ||= 2
options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent])
options[:builder].instruct! unless options.delete(:skip_instruct)
options[:builder].errors do |e|
full_messages.each { |msg| e.error(msg) }
end
end
end
# Active Records implement validation by overwriting Base#validate (or the variations, +validate_on_create+ and
# +validate_on_update+). Each of these methods can inspect the state of the object, which usually means ensuring
# that a number of attributes have a certain value (such as not empty, within a given range, matching a certain regular expression).
#
# Example:
#
# class Person < ActiveRecord::Base
# protected
# def validate
# errors.add_on_empty %w( first_name last_name )
# errors.add("phone_number", "has invalid format") unless phone_number =~ /[0-9]*/
# end
#
# def validate_on_create # is only run the first time a new object is saved
# unless valid_discount?(membership_discount)
# errors.add("membership_discount", "has expired")
# end
# end
#
# def validate_on_update
# errors.add_to_base("No changes have occurred") if unchanged_attributes?
# end
# end
#
# person = Person.new("first_name" => "David", "phone_number" => "what?")
# person.save # => false (and doesn't do the save)
# person.errors.empty? # => false
# person.errors.count # => 2
# person.errors.on "last_name" # => "can't be empty"
# person.errors.on "phone_number" # => "has invalid format"
# person.errors.each_full { |msg| puts msg }
# # => "Last name can't be empty\n" +
# "Phone number has invalid format"
#
# person.attributes = { "last_name" => "Heinemeier", "phone_number" => "555-555" }
# person.save # => true (and person is now saved in the database)
#
# An Errors object is automatically created for every Active Record.
#
# Please do have a look at ActiveRecord::Validations::ClassMethods for a higher level of validations.
module Validations
VALIDATIONS = %w( validate validate_on_create validate_on_update )
def self.included(base) # :nodoc:
base.extend ClassMethods
base.class_eval do
alias_method_chain :save, :validation
alias_method_chain :save!, :validation
alias_method_chain :update_attribute, :validation_skipping
end
base.send :include, ActiveSupport::Callbacks
base.define_callbacks *VALIDATIONS
end
# All of the following validations are defined in the class scope of the model that you're interested in validating.
# They offer a more declarative way of specifying when the model is valid and when it is not. It is recommended to use
# these over the low-level calls to +validate+ and +validate_on_create+ when possible.
module ClassMethods
DEFAULT_VALIDATION_OPTIONS = {
:on => :save,
:allow_nil => false,
:allow_blank => false,
:message => nil
}.freeze
ALL_RANGE_OPTIONS = [ :is, :within, :in, :minimum, :maximum ].freeze
ALL_NUMERICALITY_CHECKS = { :greater_than => '>', :greater_than_or_equal_to => '>=',
:equal_to => '==', :less_than => '<', :less_than_or_equal_to => '<=',
:odd => 'odd?', :even => 'even?' }.freeze
# Adds a validation method or block to the class. This is useful when
# overriding the +validate+ instance method becomes too unwieldly and
# you're looking for more descriptive declaration of your validations.
#
# This can be done with a symbol pointing to a method:
#
# class Comment < ActiveRecord::Base
# validate :must_be_friends
#
# def must_be_friends
# errors.add_to_base("Must be friends to leave a comment") unless commenter.friend_of?(commentee)
# end
# end
#
# Or with a block which is passed the current record to be validated:
#
# class Comment < ActiveRecord::Base
# validate do |comment|
# comment.must_be_friends
# end
#
# def must_be_friends
# errors.add_to_base("Must be friends to leave a comment") unless commenter.friend_of?(commentee)
# end
# end
#
# This usage applies to +validate_on_create+ and +validate_on_update+ as well.
# Validates each attribute against a block.
#
# class Person < ActiveRecord::Base
# validates_each :first_name, :last_name do |record, attr, value|
# record.errors.add attr, 'starts with z.' if value[0] == ?z
# end
# end
#
# Options:
# * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>).
# * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+.
# * <tt>:allow_blank</tt> - Skip validation if attribute is blank.
# * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
# 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. <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_each(*attrs)
options = attrs.extract_options!.symbolize_keys
attrs = attrs.flatten
# Declare the validation.
send(validation_method(options[:on] || :save), options) do |record|
attrs.each do |attr|
value = record.send(attr)
next if (value.nil? && options[:allow_nil]) || (value.blank? && options[:allow_blank])
yield record, attr, value
end
end
end
# Encapsulates the pattern of wanting to validate a password or email address field with a confirmation. Example:
#
# Model:
# class Person < ActiveRecord::Base
# validates_confirmation_of :user_name, :password
# validates_confirmation_of :email_address, :message => "should match confirmation"
# end
#
# View:
# <%= password_field "person", "password" %>
# <%= password_field "person", "password_confirmation" %>
#
# The added +password_confirmation+ attribute is virtual; it exists only as an in-memory attribute for validating the password.
# To achieve this, the validation adds accessors to the model for the confirmation attribute. NOTE: This check is performed
# only if +password_confirmation+ is not +nil+, and by default only on save. To require confirmation, make sure to add a presence
# check for the confirmation attribute:
#
# validates_presence_of :password_confirmation, :if => :password_changed?
#
# Configuration options:
# * <tt>:message</tt> - A custom error message (default is: "doesn't match confirmation").
# * <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. <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. <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_confirmation_of(*attr_names)
configuration = { :message => ActiveRecord::Errors.default_error_messages[:confirmation], :on => :save }
configuration.update(attr_names.extract_options!)
attr_accessor(*(attr_names.map { |n| "#{n}_confirmation" }))
validates_each(attr_names, configuration) do |record, attr_name, value|
record.errors.add(attr_name, configuration[:message]) unless record.send("#{attr_name}_confirmation").nil? or value == record.send("#{attr_name}_confirmation")
end
end
# Encapsulates the pattern of wanting to validate the acceptance of a terms of service check box (or similar agreement). Example:
#
# class Person < ActiveRecord::Base
# validates_acceptance_of :terms_of_service
# validates_acceptance_of :eula, :message => "must be abided"
# end
#
# If the database column does not exist, the +terms_of_service+ attribute is entirely virtual. This check is
# performed only if +terms_of_service+ is not +nil+ and by default on save.
#
# Configuration options:
# * <tt>:message</tt> - A custom error message (default is: "must be accepted").
# * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>).
# * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+ (default is true).
# * <tt>:accept</tt> - Specifies value that is considered accepted. The default value is a string "1", which
# makes it easy to relate to an HTML checkbox. This should be set to +true+ if you are validating a database
# column, since the attribute is typecast from "1" to +true+ before validation.
# * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
# 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. <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_acceptance_of(*attr_names)
configuration = { :message => ActiveRecord::Errors.default_error_messages[:accepted], :on => :save, :allow_nil => true, :accept => "1" }
configuration.update(attr_names.extract_options!)
db_cols = begin
column_names
rescue ActiveRecord::StatementInvalid
[]
end
names = attr_names.reject { |name| db_cols.include?(name.to_s) }
attr_accessor(*names)
validates_each(attr_names,configuration) do |record, attr_name, value|
record.errors.add(attr_name, configuration[:message]) unless value == configuration[:accept]
end
end
# Validates that the specified attributes are not blank (as defined by Object#blank?). Happens by default on save. Example:
#
# class Person < ActiveRecord::Base
# validates_presence_of :first_name
# end
#
# 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
#
# 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>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.
# * <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.
#
def validates_presence_of(*attr_names)
configuration = { :message => ActiveRecord::Errors.default_error_messages[:blank], :on => :save }
configuration.update(attr_names.extract_options!)
# can't use validates_each here, because it cannot cope with nonexistent attributes,
# while errors.add_on_empty can
send(validation_method(configuration[:on]), configuration) do |record|
record.errors.add_on_blank(attr_names, configuration[:message])
end
end
# Validates that the specified attribute matches the length restrictions supplied. Only one option can be used at a time:
#
# class Person < ActiveRecord::Base
# validates_length_of :first_name, :maximum=>30
# validates_length_of :last_name, :maximum=>30, :message=>"less than %d if you don't mind"
# validates_length_of :fax, :in => 7..32, :allow_nil => true
# validates_length_of :phone, :in => 7..32, :allow_blank => true
# validates_length_of :user_name, :within => 6..20, :too_long => "pick a shorter name", :too_short => "pick a longer name"
# validates_length_of :fav_bra_size, :minimum=>1, :too_short=>"please enter at least %d character"
# validates_length_of :smurf_leader, :is=>4, :message=>"papa is spelled with %d characters... don't play me."
# end
#
# Configuration options:
# * <tt>:minimum</tt> - The minimum size of the attribute.
# * <tt>:maximum</tt> - The maximum size of the attribute.
# * <tt>:is</tt> - The exact size of the attribute.
# * <tt>:within</tt> - A range specifying the minimum and maximum size of the attribute.
# * <tt>:in</tt> - A synonym(or alias) for <tt>:within</tt>.
# * <tt>:allow_nil</tt> - Attribute may be +nil+; skip validation.
# * <tt>:allow_blank</tt> - Attribute may be blank; skip validation.
#
# * <tt>:too_long</tt> - The error message if the attribute goes over the maximum (default is: "is too long (maximum is %d characters)").
# * <tt>:too_short</tt> - The error message if the attribute goes under the minimum (default is: "is too short (min is %d characters)").
# * <tt>:wrong_length</tt> - The error message if using the <tt>:is</tt> method and the attribute is the wrong size (default is: "is the wrong length (should be %d characters)").
# * <tt>:message</tt> - The error message to use for a <tt>:minimum</tt>, <tt>:maximum</tt>, or <tt>:is</tt> violation. An alias of the appropriate <tt>too_long</tt>/<tt>too_short</tt>/<tt>wrong_length</tt> message.
# * <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. <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. <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_length_of(*attrs)
# Merge given options with defaults.
options = {
:too_long => ActiveRecord::Errors.default_error_messages[:too_long],
:too_short => ActiveRecord::Errors.default_error_messages[:too_short],
:wrong_length => ActiveRecord::Errors.default_error_messages[:wrong_length]
}.merge(DEFAULT_VALIDATION_OPTIONS)
options.update(attrs.extract_options!.symbolize_keys)
# Ensure that one and only one range option is specified.
range_options = ALL_RANGE_OPTIONS & options.keys
case range_options.size
when 0
raise ArgumentError, 'Range unspecified. Specify the :within, :maximum, :minimum, or :is option.'
when 1
# Valid number of options; do nothing.
else
raise ArgumentError, 'Too many range options specified. Choose only one.'
end
# Get range option and value.
option = range_options.first
option_value = options[range_options.first]
case option
when :within, :in
raise ArgumentError, ":#{option} must be a Range" unless option_value.is_a?(Range)
too_short = options[:too_short] % option_value.begin
too_long = options[:too_long] % option_value.end
validates_each(attrs, options) do |record, attr, value|
value = value.split(//) if value.kind_of?(String)
if value.nil? or value.size < option_value.begin
record.errors.add(attr, too_short)
elsif value.size > option_value.end
record.errors.add(attr, too_long)
end
end
when :is, :minimum, :maximum
raise ArgumentError, ":#{option} must be a nonnegative Integer" unless option_value.is_a?(Integer) and option_value >= 0
# Declare different validations per option.
validity_checks = { :is => "==", :minimum => ">=", :maximum => "<=" }
message_options = { :is => :wrong_length, :minimum => :too_short, :maximum => :too_long }
message = (options[:message] || options[message_options[option]]) % option_value
validates_each(attrs, options) do |record, attr, value|
value = value.split(//) if value.kind_of?(String)
record.errors.add(attr, message) unless !value.nil? and value.size.method(validity_checks[option])[option_value]
end
end
end
alias_method :validates_size_of, :validates_length_of
# Validates whether the value of the specified attributes are unique across the system. Useful for making sure that only one user
# can be named "davidhh".
#
# class Person < ActiveRecord::Base
# validates_uniqueness_of :user_name, :scope => :account_id
# end
#
# It can also validate whether the value of the specified attributes are unique based on multiple scope parameters. For example,
# making sure that a teacher can only be on the schedule once per semester for a particular class.
#
# class TeacherSchedule < ActiveRecord::Base
# validates_uniqueness_of :teacher_id, :scope => [:semester_id, :class_id]
# end
#
# When the record is created, a check is performed to make sure that no record exists in the database with the given value for the specified
# attribute (that maps to a column). When the record is updated, the same check is made but disregarding the record itself.
#
# Because this check is performed outside the database there is still a chance that duplicate values
# will be inserted in two parallel transactions. To guarantee against this you should create a
# unique index on the field. See +add_index+ for more information.
#
# Configuration options:
# * <tt>:message</tt> - Specifies a custom error message (default is: "has already been taken").
# * <tt>:scope</tt> - One or more columns by which to limit the scope of the uniqueness constraint.
# * <tt>:case_sensitive</tt> - Looks for an exact match. Ignored by non-text columns (+false+ by default).
# * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute is +nil+ (default is +false+).
# * <tt>:allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is +false+).
# * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
# 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. <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_uniqueness_of(*attr_names)
configuration = { :message => ActiveRecord::Errors.default_error_messages[:taken], :case_sensitive => true }
configuration.update(attr_names.extract_options!)
validates_each(attr_names,configuration) do |record, attr_name, value|
# The check for an existing value should be run from a class that
# isn't abstract. This means working down from the current class
# (self), to the first non-abstract class. Since classes don't know
# their subclasses, we have to build the hierarchy between self and
# the record's class.
class_hierarchy = [record.class]
while class_hierarchy.first != self
class_hierarchy.insert(0, class_hierarchy.first.superclass)
end
# Now we can work our way down the tree to the first non-abstract
# class (which has a database table to query from).
finder_class = class_hierarchy.detect { |klass| !klass.abstract_class? }
if value.nil? || (configuration[:case_sensitive] || !finder_class.columns_hash[attr_name.to_s].text?)
condition_sql = "#{record.class.quoted_table_name}.#{attr_name} #{attribute_condition(value)}"
condition_params = [value]
else
# sqlite has case sensitive SELECT query, while MySQL/Postgresql don't.
# Hence, this is needed only for sqlite.
condition_sql = "LOWER(#{record.class.quoted_table_name}.#{attr_name}) #{attribute_condition(value)}"
condition_params = [value.downcase]
end
if scope = configuration[:scope]
Array(scope).map do |scope_item|
scope_value = record.send(scope_item)
condition_sql << " AND #{record.class.quoted_table_name}.#{scope_item} #{attribute_condition(scope_value)}"
condition_params << scope_value
end
end
unless record.new_record?
condition_sql << " AND #{record.class.quoted_table_name}.#{record.class.primary_key} <> ?"
condition_params << record.send(:id)
end
results = finder_class.with_exclusive_scope do
connection.select_all(
construct_finder_sql(
:select => "#{connection.quote_column_name(attr_name)}",
:from => "#{finder_class.quoted_table_name}",
:conditions => [condition_sql, *condition_params]
)
)
end
unless results.length.zero?
found = true
# As MySQL/Postgres don't have case sensitive SELECT queries, we try to find duplicate
# column in ruby when case sensitive option
if configuration[:case_sensitive] && finder_class.columns_hash[attr_name.to_s].text?
found = results.any? { |a| a[attr_name.to_s] == value }
end
record.errors.add(attr_name, configuration[:message]) if found
end
end
end
# Validates whether the value of the specified attribute is of the correct form by matching it against the regular expression
# provided.
#
# class Person < ActiveRecord::Base
# validates_format_of :email, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i, :on => :create
# end
#
# Note: use <tt>\A</tt> and <tt>\Z</tt> to match the start and end of the string, <tt>^</tt> and <tt>$</tt> match the start/end of a line.
#
# A regular expression must be provided or else an exception will be raised.
#
# Configuration options:
# * <tt>:message</tt> - A custom error message (default is: "is invalid").
# * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute is +nil+ (default is +false+).
# * <tt>:allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is +false+).
# * <tt>:with</tt> - The regular expression used to validate the format with (note: must be supplied!).
# * <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. <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. <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_format_of(*attr_names)
configuration = { :message => ActiveRecord::Errors.default_error_messages[:invalid], :on => :save, :with => nil }
configuration.update(attr_names.extract_options!)
raise(ArgumentError, "A regular expression must be supplied as the :with option of the configuration hash") unless configuration[:with].is_a?(Regexp)
validates_each(attr_names, configuration) do |record, attr_name, value|
record.errors.add(attr_name, configuration[:message] % value) unless value.to_s =~ configuration[:with]
end
end
# Validates whether the value of the specified attribute is available in a particular enumerable object.
#
# class Person < ActiveRecord::Base
# validates_inclusion_of :gender, :in => %w( m f ), :message => "woah! what are you then!??!!"
# validates_inclusion_of :age, :in => 0..99
# validates_inclusion_of :format, :in => %w( jpg gif png ), :message => "extension %s is not included in the list"
# end
#
# Configuration options:
# * <tt>:in</tt> - An enumerable object of available items.
# * <tt>:message</tt> - Specifies a custom error message (default is: "is not included in the list").
# * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute is +nil+ (default is +false+).
# * <tt>:allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is +false+).
# * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
# 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. <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_inclusion_of(*attr_names)
configuration = { :message => ActiveRecord::Errors.default_error_messages[:inclusion], :on => :save }
configuration.update(attr_names.extract_options!)
enum = configuration[:in] || configuration[:within]
raise(ArgumentError, "An object with the method include? is required must be supplied as the :in option of the configuration hash") unless enum.respond_to?("include?")
validates_each(attr_names, configuration) do |record, attr_name, value|
record.errors.add(attr_name, configuration[:message] % value) unless enum.include?(value)
end
end
# Validates that the value of the specified attribute is not in a particular enumerable object.
#
# class Person < ActiveRecord::Base
# validates_exclusion_of :username, :in => %w( admin superuser ), :message => "You don't belong here"
# validates_exclusion_of :age, :in => 30..60, :message => "This site is only for under 30 and over 60"
# validates_exclusion_of :format, :in => %w( mov avi ), :message => "extension %s is not allowed"
# end
#
# Configuration options:
# * <tt>:in</tt> - An enumerable object of items that the value shouldn't be part of.
# * <tt>:message</tt> - Specifies a custom error message (default is: "is reserved").
# * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute is +nil+ (default is +false+).
# * <tt>:allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is +false+).
# * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
# 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. <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_exclusion_of(*attr_names)
configuration = { :message => ActiveRecord::Errors.default_error_messages[:exclusion], :on => :save }
configuration.update(attr_names.extract_options!)
enum = configuration[:in] || configuration[:within]
raise(ArgumentError, "An object with the method include? is required must be supplied as the :in option of the configuration hash") unless enum.respond_to?("include?")
validates_each(attr_names, configuration) do |record, attr_name, value|
record.errors.add(attr_name, configuration[:message] % value) if enum.include?(value)
end
end
# Validates whether the associated object or objects are all valid themselves. Works with any kind of association.
#
# class Book < ActiveRecord::Base
# has_many :pages
# belongs_to :library
#
# validates_associated :pages, :library
# end
#
# Warning: If, after the above definition, you then wrote:
#
# class Page < ActiveRecord::Base
# belongs_to :book
#
# validates_associated :book
# end
#
# this would specify a circular dependency and cause infinite recursion.
#
# NOTE: This validation will not fail if the association hasn't been assigned. If you want to ensure that the association
# is both present and guaranteed to be valid, you also need to use +validates_presence_of+.
#
# Configuration options:
# * <tt>:message</tt> - A custom error message (default is: "is invalid")
# * <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. <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. <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_associated(*attr_names)
configuration = { :message => ActiveRecord::Errors.default_error_messages[:invalid], :on => :save }
configuration.update(attr_names.extract_options!)
validates_each(attr_names, configuration) do |record, attr_name, value|
record.errors.add(attr_name, configuration[:message]) unless
(value.is_a?(Array) ? value : [value]).inject(true) { |v, r| (r.nil? || r.valid?) && v }
end
end
# Validates whether the value of the specified attribute is numeric by trying to convert it to
# a float with Kernel.Float (if <tt>only_integer</tt> is false) or applying it to the regular expression
# <tt>/\A[\+\-]?\d+\Z/</tt> (if <tt>only_integer</tt> is set to true).
#
# class Person < ActiveRecord::Base
# validates_numericality_of :value, :on => :create
# end
#
# Configuration options:
# * <tt>:message</tt> - A custom error message (default is: "is not a number").
# * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>).
# * <tt>:only_integer</tt> - Specifies whether the value has to be an integer, e.g. an integral value (default is +false+).
# * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+ (default is +false+). Notice that for fixnum and float columns empty strings are converted to +nil+.
# * <tt>:greater_than</tt> - Specifies the value must be greater than the supplied value.
# * <tt>:greater_than_or_equal_to</tt> - Specifies the value must be greater than or equal the supplied value.
# * <tt>:equal_to</tt> - Specifies the value must be equal to the supplied value.
# * <tt>:less_than</tt> - Specifies the value must be less than the supplied value.
# * <tt>:less_than_or_equal_to</tt> - Specifies the value must be less than or equal the supplied value.
# * <tt>:odd</tt> - Specifies the value must be an odd number.
# * <tt>:even</tt> - Specifies the value must be an even number.
# * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
# 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. <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_numericality_of(*attr_names)
configuration = { :on => :save, :only_integer => false, :allow_nil => false }
configuration.update(attr_names.extract_options!)
numericality_options = ALL_NUMERICALITY_CHECKS.keys & configuration.keys
(numericality_options - [ :odd, :even ]).each do |option|
raise ArgumentError, ":#{option} must be a number" unless configuration[option].is_a?(Numeric)
end
validates_each(attr_names,configuration) do |record, attr_name, value|
raw_value = record.send("#{attr_name}_before_type_cast") || value
next if configuration[:allow_nil] and raw_value.nil?
if configuration[:only_integer]
unless raw_value.to_s =~ /\A[+-]?\d+\Z/
record.errors.add(attr_name, configuration[:message] || ActiveRecord::Errors.default_error_messages[:not_a_number])
next
end
raw_value = raw_value.to_i
else
begin
raw_value = Kernel.Float(raw_value.to_s)
rescue ArgumentError, TypeError
record.errors.add(attr_name, configuration[:message] || ActiveRecord::Errors.default_error_messages[:not_a_number])
next
end
end
numericality_options.each do |option|
case option
when :odd, :even
record.errors.add(attr_name, configuration[:message] || ActiveRecord::Errors.default_error_messages[option]) unless raw_value.to_i.method(ALL_NUMERICALITY_CHECKS[option])[]
else
message = configuration[:message] || ActiveRecord::Errors.default_error_messages[option]
message = message % configuration[option] if configuration[option]
record.errors.add(attr_name, message) unless raw_value.method(ALL_NUMERICALITY_CHECKS[option])[configuration[option]]
end
end
end
end
# Creates an object just like Base.create but calls save! instead of save
# so an exception is raised if the record is invalid.
def create!(attributes = nil, &block)
if attributes.is_a?(Array)
attributes.collect { |attr| create!(attr, &block) }
else
object = new(attributes)
yield(object) if block_given?
object.save!
object
end
end
private
def validation_method(on)
case on
when :save then :validate
when :create then :validate_on_create
when :update then :validate_on_update
end
end
end
# The validation process on save can be skipped by passing false. The regular Base#save method is
# replaced with this when the validations module is mixed in, which it is by default.
def save_with_validation(perform_validation = true)
if perform_validation && valid? || !perform_validation
save_without_validation
else
false
end
end
# Attempts to save the record just like Base#save but will raise a RecordInvalid exception instead of returning false
# if the record is not valid.
def save_with_validation!
if valid?
save_without_validation!
else
raise RecordInvalid.new(self)
end
end
# Updates a single attribute and saves the record without going through the normal validation procedure.
# This is especially useful for boolean flags on existing records. The regular +update_attribute+ method
# in Base is replaced with this when the validations module is mixed in, which it is by default.
def update_attribute_with_validation_skipping(name, value)
send(name.to_s + '=', value)
save(false)
end
# Runs +validate+ and +validate_on_create+ or +validate_on_update+ and returns true if no errors were added otherwise false.
def valid?
errors.clear
run_callbacks(:validate)
validate
if new_record?
run_callbacks(:validate_on_create)
validate_on_create
else
run_callbacks(:validate_on_update)
validate_on_update
end
errors.empty?
end
# Returns the Errors object that holds all information about attribute error messages.
def errors
@errors ||= Errors.new(self)
end
protected
# Overwrite this method for validation checks on all saves and use <tt>Errors.add(field, msg)</tt> for invalid attributes.
def validate #:doc:
end
# Overwrite this method for validation checks used only on creation.
def validate_on_create #:doc:
end
# Overwrite this method for validation checks used only on updates.
def validate_on_update # :doc:
end
end
end

View file

@ -1,362 +0,0 @@
require 'db2/db2cli.rb'
module DB2
module DB2Util
include DB2CLI
def free() SQLFreeHandle(@handle_type, @handle); end
def handle() @handle; end
def check_rc(rc)
if ![SQL_SUCCESS, SQL_SUCCESS_WITH_INFO, SQL_NO_DATA_FOUND].include?(rc)
rec = 1
msg = ''
loop do
a = SQLGetDiagRec(@handle_type, @handle, rec, 500)
break if a[0] != SQL_SUCCESS
msg << a[3] if !a[3].nil? and a[3] != '' # Create message.
rec += 1
end
raise "DB2 error: #{msg}"
end
end
end
class Environment
include DB2Util
def initialize
@handle_type = SQL_HANDLE_ENV
rc, @handle = SQLAllocHandle(@handle_type, SQL_NULL_HANDLE)
check_rc(rc)
end
def data_sources(buffer_length = 1024)
retval = []
max_buffer_length = buffer_length
a = SQLDataSources(@handle, SQL_FETCH_FIRST, SQL_MAX_DSN_LENGTH + 1, buffer_length)
retval << [a[1], a[3]]
max_buffer_length = [max_buffer_length, a[4]].max
loop do
a = SQLDataSources(@handle, SQL_FETCH_NEXT, SQL_MAX_DSN_LENGTH + 1, buffer_length)
break if a[0] == SQL_NO_DATA_FOUND
retval << [a[1], a[3]]
max_buffer_length = [max_buffer_length, a[4]].max
end
if max_buffer_length > buffer_length
get_data_sources(max_buffer_length)
else
retval
end
end
end
class Connection
include DB2Util
def initialize(environment)
@env = environment
@handle_type = SQL_HANDLE_DBC
rc, @handle = SQLAllocHandle(@handle_type, @env.handle)
check_rc(rc)
end
def connect(server_name, user_name = '', auth = '')
check_rc(SQLConnect(@handle, server_name, user_name.to_s, auth.to_s))
end
def set_connect_attr(attr, value)
value += "\0" if value.class == String
check_rc(SQLSetConnectAttr(@handle, attr, value))
end
def set_auto_commit_on
set_connect_attr(SQL_ATTR_AUTOCOMMIT, SQL_AUTOCOMMIT_ON)
end
def set_auto_commit_off
set_connect_attr(SQL_ATTR_AUTOCOMMIT, SQL_AUTOCOMMIT_OFF)
end
def disconnect
check_rc(SQLDisconnect(@handle))
end
def rollback
check_rc(SQLEndTran(@handle_type, @handle, SQL_ROLLBACK))
end
def commit
check_rc(SQLEndTran(@handle_type, @handle, SQL_COMMIT))
end
end
class Statement
include DB2Util
def initialize(connection)
@conn = connection
@handle_type = SQL_HANDLE_STMT
@parms = [] #yun
@sql = '' #yun
@numParms = 0 #yun
@prepared = false #yun
@parmArray = [] #yun. attributes of the parameter markers
rc, @handle = SQLAllocHandle(@handle_type, @conn.handle)
check_rc(rc)
end
def columns(table_name, schema_name = '%')
check_rc(SQLColumns(@handle, '', schema_name.upcase, table_name.upcase, '%'))
fetch_all
end
def tables(schema_name = '%')
check_rc(SQLTables(@handle, '', schema_name.upcase, '%', 'TABLE'))
fetch_all
end
def indexes(table_name, schema_name = '')
check_rc(SQLStatistics(@handle, '', schema_name.upcase, table_name.upcase, SQL_INDEX_ALL, SQL_ENSURE))
fetch_all
end
def prepare(sql)
@sql = sql
check_rc(SQLPrepare(@handle, sql))
rc, @numParms = SQLNumParams(@handle) #number of question marks
check_rc(rc)
#--------------------------------------------------------------------------
# parameter attributes are stored in instance variable @parmArray so that
# they are available when execute method is called.
#--------------------------------------------------------------------------
if @numParms > 0 # get parameter marker attributes
1.upto(@numParms) do |i| # parameter number starts from 1
rc, type, size, decimalDigits = SQLDescribeParam(@handle, i)
check_rc(rc)
@parmArray << Parameter.new(type, size, decimalDigits)
end
end
@prepared = true
self
end
def execute(*parms)
raise "The statement was not prepared" if @prepared == false
if parms.size == 1 and parms[0].class == Array
parms = parms[0]
end
if @numParms != parms.size
raise "Number of parameters supplied does not match with the SQL statement"
end
if @numParms > 0 #need to bind parameters
#--------------------------------------------------------------------
#calling bindParms may not be safe. Look comment below.
#--------------------------------------------------------------------
#bindParms(parms)
valueArray = []
1.upto(@numParms) do |i| # parameter number starts from 1
type = @parmArray[i - 1].class
size = @parmArray[i - 1].size
decimalDigits = @parmArray[i - 1].decimalDigits
if parms[i - 1].class == String
valueArray << parms[i - 1]
else
valueArray << parms[i - 1].to_s
end
rc = SQLBindParameter(@handle, i, type, size, decimalDigits, valueArray[i - 1])
check_rc(rc)
end
end
check_rc(SQLExecute(@handle))
if @numParms != 0
check_rc(SQLFreeStmt(@handle, SQL_RESET_PARAMS)) # Reset parameters
end
self
end
#-------------------------------------------------------------------------------
# The last argument(value) to SQLBindParameter is a deferred argument, that is,
# it should be available when SQLExecute is called. Even though "value" is
# local to bindParms method, it seems that it is available when SQLExecute
# is called. I am not sure whether it would still work if garbage collection
# is done between bindParms call and SQLExecute call inside the execute method
# above.
#-------------------------------------------------------------------------------
def bindParms(parms) # This is the real thing. It uses SQLBindParms
1.upto(@numParms) do |i| # parameter number starts from 1
rc, dataType, parmSize, decimalDigits = SQLDescribeParam(@handle, i)
check_rc(rc)
if parms[i - 1].class == String
value = parms[i - 1]
else
value = parms[i - 1].to_s
end
rc = SQLBindParameter(@handle, i, dataType, parmSize, decimalDigits, value)
check_rc(rc)
end
end
#------------------------------------------------------------------------------
# bind method does not use DB2's SQLBindParams, but replaces "?" in the
# SQL statement with the value before passing the SQL statement to DB2.
# It is not efficient and can handle only strings since it puts everything in
# quotes.
#------------------------------------------------------------------------------
def bind(sql, args) #does not use SQLBindParams
arg_index = 0
result = ""
tokens(sql).each do |part|
case part
when '?'
result << "'" + (args[arg_index]) + "'" #put it into quotes
arg_index += 1
when '??'
result << "?"
else
result << part
end
end
if arg_index < args.size
raise "Too many SQL parameters"
elsif arg_index > args.size
raise "Not enough SQL parameters"
end
result
end
## Break the sql string into parts.
#
# This is NOT a full lexer for SQL. It just breaks up the SQL
# string enough so that question marks, double question marks and
# quoted strings are separated. This is used when binding
# arguments to "?" in the SQL string. Note: comments are not
# handled.
#
def tokens(sql)
toks = sql.scan(/('([^'\\]|''|\\.)*'|"([^"\\]|""|\\.)*"|\?\??|[^'"?]+)/)
toks.collect { |t| t[0] }
end
def exec_direct(sql)
check_rc(SQLExecDirect(@handle, sql))
self
end
def set_cursor_name(name)
check_rc(SQLSetCursorName(@handle, name))
self
end
def get_cursor_name
rc, name = SQLGetCursorName(@handle)
check_rc(rc)
name
end
def row_count
rc, rowcount = SQLRowCount(@handle)
check_rc(rc)
rowcount
end
def num_result_cols
rc, cols = SQLNumResultCols(@handle)
check_rc(rc)
cols
end
def fetch_all
if block_given?
while row = fetch do
yield row
end
else
res = []
while row = fetch do
res << row
end
res
end
end
def fetch
cols = get_col_desc
rc = SQLFetch(@handle)
if rc == SQL_NO_DATA_FOUND
SQLFreeStmt(@handle, SQL_CLOSE) # Close cursor
SQLFreeStmt(@handle, SQL_RESET_PARAMS) # Reset parameters
return nil
end
raise "ERROR" unless rc == SQL_SUCCESS
retval = []
cols.each_with_index do |c, i|
rc, content = SQLGetData(@handle, i + 1, c[1], c[2] + 1) #yun added 1 to c[2]
retval << adjust_content(content)
end
retval
end
def fetch_as_hash
cols = get_col_desc
rc = SQLFetch(@handle)
if rc == SQL_NO_DATA_FOUND
SQLFreeStmt(@handle, SQL_CLOSE) # Close cursor
SQLFreeStmt(@handle, SQL_RESET_PARAMS) # Reset parameters
return nil
end
raise "ERROR" unless rc == SQL_SUCCESS
retval = {}
cols.each_with_index do |c, i|
rc, content = SQLGetData(@handle, i + 1, c[1], c[2] + 1) #yun added 1 to c[2]
retval[c[0]] = adjust_content(content)
end
retval
end
def get_col_desc
rc, nr_cols = SQLNumResultCols(@handle)
cols = (1..nr_cols).collect do |c|
rc, name, bl, type, col_sz = SQLDescribeCol(@handle, c, 1024)
[name.downcase, type, col_sz]
end
end
def adjust_content(c)
case c.class.to_s
when 'DB2CLI::NullClass'
return nil
when 'DB2CLI::Time'
"%02d:%02d:%02d" % [c.hour, c.minute, c.second]
when 'DB2CLI::Date'
"%04d-%02d-%02d" % [c.year, c.month, c.day]
when 'DB2CLI::Timestamp'
"%04d-%02d-%02d %02d:%02d:%02d" % [c.year, c.month, c.day, c.hour, c.minute, c.second]
else
return c
end
end
end
class Parameter
attr_reader :type, :size, :decimalDigits
def initialize(type, size, decimalDigits)
@type, @size, @decimalDigits = type, size, decimalDigits
end
end
end

File diff suppressed because it is too large Load diff

View file

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

View file

@ -1 +0,0 @@
require 'active_record'

View file

@ -1 +0,0 @@
# Logfile created on Wed Oct 31 16:05:13 +0000 2007 by logger.rb/1.5.2.9

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

View file

@ -1,24 +0,0 @@
# The filename begins with "aaa" to ensure this is the first test.
require "cases/helper"
class AAACreateTablesTest < ActiveRecord::TestCase
self.use_transactional_fixtures = false
def test_load_schema
eval(File.read(SCHEMA_ROOT + "/schema.rb"))
if File.exists?(adapter_specific_schema_file)
eval(File.read(adapter_specific_schema_file))
end
assert true
end
def test_drop_and_create_courses_table
eval(File.read(SCHEMA_ROOT + "/schema2.rb"))
assert true
end
private
def adapter_specific_schema_file
SCHEMA_ROOT + '/' + ActiveRecord::Base.connection.adapter_name.downcase + '_specific_schema.rb'
end
end

View file

@ -1,95 +0,0 @@
require "cases/helper"
class ActiveSchemaTest < ActiveRecord::TestCase
def setup
ActiveRecord::ConnectionAdapters::MysqlAdapter.class_eval do
alias_method :execute_without_stub, :execute
def execute(sql, name = nil) return sql end
end
end
def teardown
ActiveRecord::ConnectionAdapters::MysqlAdapter.class_eval do
remove_method :execute
alias_method :execute, :execute_without_stub
end
end
def test_drop_table
assert_equal "DROP TABLE `people`", drop_table(:people)
end
if current_adapter?(:MysqlAdapter)
def test_create_mysql_database_with_encoding
assert_equal "CREATE DATABASE `matt` DEFAULT CHARACTER SET `utf8`", create_database(:matt)
assert_equal "CREATE DATABASE `aimonetti` DEFAULT CHARACTER SET `latin1`", create_database(:aimonetti, {:charset => 'latin1'})
assert_equal "CREATE DATABASE `matt_aimonetti` DEFAULT CHARACTER SET `big5` COLLATE `big5_chinese_ci`", create_database(:matt_aimonetti, {:charset => :big5, :collation => :big5_chinese_ci})
end
end
def test_add_column
assert_equal "ALTER TABLE `people` ADD `last_name` varchar(255)", add_column(:people, :last_name, :string)
end
def test_add_column_with_limit
assert_equal "ALTER TABLE `people` ADD `key` varchar(32)", add_column(:people, :key, :string, :limit => 32)
end
def test_drop_table_with_specific_database
assert_equal "DROP TABLE `otherdb`.`people`", drop_table('otherdb.people')
end
def test_add_timestamps
with_real_execute do
begin
ActiveRecord::Base.connection.create_table :delete_me do |t|
end
ActiveRecord::Base.connection.add_timestamps :delete_me
assert column_present?('delete_me', 'updated_at', 'datetime')
assert column_present?('delete_me', 'created_at', 'datetime')
ensure
ActiveRecord::Base.connection.drop_table :delete_me rescue nil
end
end
end
def test_remove_timestamps
with_real_execute do
begin
ActiveRecord::Base.connection.create_table :delete_me do |t|
t.timestamps
end
ActiveRecord::Base.connection.remove_timestamps :delete_me
assert !column_present?('delete_me', 'updated_at', 'datetime')
assert !column_present?('delete_me', 'created_at', 'datetime')
ensure
ActiveRecord::Base.connection.drop_table :delete_me rescue nil
end
end
end
private
def with_real_execute
#we need to actually modify some data, so we make execute point to the original method
ActiveRecord::ConnectionAdapters::MysqlAdapter.class_eval do
alias_method :execute_with_stub, :execute
alias_method :execute, :execute_without_stub
end
yield
ensure
#before finishing, we restore the alias to the mock-up method
ActiveRecord::ConnectionAdapters::MysqlAdapter.class_eval do
alias_method :execute, :execute_with_stub
end
end
def method_missing(method_symbol, *arguments)
ActiveRecord::Base.connection.send(method_symbol, *arguments)
end
def column_present?(table_name, column_name, type)
results = ActiveRecord::Base.connection.select_all("SHOW FIELDS FROM #{table_name} LIKE '#{column_name}'")
results.first && results.first['Type'] == type
end
end

View file

@ -1,24 +0,0 @@
require 'cases/helper'
class PostgresqlActiveSchemaTest < Test::Unit::TestCase
def setup
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.class_eval do
alias_method :real_execute, :execute
def execute(sql, name = nil) sql end
end
end
def teardown
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send(:alias_method, :execute, :real_execute)
end
def test_create_database_with_encoding
assert_equal "CREATE DATABASE matt ENCODING = 'utf8'", create_database(:matt)
assert_equal "CREATE DATABASE aimonetti ENCODING = 'latin1'", create_database(:aimonetti, :encoding => :latin1)
end
private
def method_missing(method_symbol, *arguments)
ActiveRecord::Base.connection.send(method_symbol, *arguments)
end
end

View file

@ -1,127 +0,0 @@
require "cases/helper"
class AdapterTest < ActiveRecord::TestCase
def setup
@connection = ActiveRecord::Base.connection
end
def test_tables
tables = @connection.tables
assert tables.include?("accounts")
assert tables.include?("authors")
assert tables.include?("tasks")
assert tables.include?("topics")
end
def test_table_exists?
assert @connection.table_exists?("accounts")
assert !@connection.table_exists?("nonexistingtable")
end
def test_indexes
idx_name = "accounts_idx"
if @connection.respond_to?(:indexes)
indexes = @connection.indexes("accounts")
assert indexes.empty?
@connection.add_index :accounts, :firm_id, :name => idx_name
indexes = @connection.indexes("accounts")
assert_equal "accounts", indexes.first.table
# OpenBase does not have the concept of a named index
# Indexes are merely properties of columns.
assert_equal idx_name, indexes.first.name unless current_adapter?(:OpenBaseAdapter)
assert !indexes.first.unique
assert_equal ["firm_id"], indexes.first.columns
else
warn "#{@connection.class} does not respond to #indexes"
end
ensure
@connection.remove_index(:accounts, :name => idx_name) rescue nil
end
def test_current_database
if @connection.respond_to?(:current_database)
assert_equal ENV['ARUNIT_DB_NAME'] || "activerecord_unittest", @connection.current_database
end
end
if current_adapter?(:MysqlAdapter)
def test_charset
assert_not_nil @connection.charset
assert_not_equal 'character_set_database', @connection.charset
assert_equal @connection.show_variable('character_set_database'), @connection.charset
end
def test_collation
assert_not_nil @connection.collation
assert_not_equal 'collation_database', @connection.collation
assert_equal @connection.show_variable('collation_database'), @connection.collation
end
def test_show_nonexistent_variable_returns_nil
assert_nil @connection.show_variable('foo_bar_baz')
end
end
def test_table_alias
def @connection.test_table_alias_length() 10; end
class << @connection
alias_method :old_table_alias_length, :table_alias_length
alias_method :table_alias_length, :test_table_alias_length
end
assert_equal 'posts', @connection.table_alias_for('posts')
assert_equal 'posts_comm', @connection.table_alias_for('posts_comments')
assert_equal 'dbo_posts', @connection.table_alias_for('dbo.posts')
class << @connection
remove_method :table_alias_length
alias_method :table_alias_length, :old_table_alias_length
end
end
# test resetting sequences in odd tables in postgreSQL
if ActiveRecord::Base.connection.respond_to?(:reset_pk_sequence!)
require 'models/movie'
require 'models/subscriber'
def test_reset_empty_table_with_custom_pk
Movie.delete_all
Movie.connection.reset_pk_sequence! 'movies'
assert_equal 1, Movie.create(:name => 'fight club').id
end
if ActiveRecord::Base.connection.adapter_name != "FrontBase"
def test_reset_table_with_non_integer_pk
Subscriber.delete_all
Subscriber.connection.reset_pk_sequence! 'subscribers'
sub = Subscriber.new(:name => 'robert drake')
sub.id = 'bob drake'
assert_nothing_raised { sub.save! }
end
end
end
def test_add_limit_offset_should_sanitize_sql_injection_for_limit_without_comas
sql_inject = "1 select * from schema"
assert_equal " LIMIT 1", @connection.add_limit_offset!("", :limit=>sql_inject)
if current_adapter?(:MysqlAdapter)
assert_equal " LIMIT 7, 1", @connection.add_limit_offset!("", :limit=>sql_inject, :offset=>7)
else
assert_equal " LIMIT 1 OFFSET 7", @connection.add_limit_offset!("", :limit=>sql_inject, :offset=>7)
end
end
def test_add_limit_offset_should_sanitize_sql_injection_for_limit_with_comas
sql_inject = "1, 7 procedure help()"
if current_adapter?(:MysqlAdapter)
assert_equal " LIMIT 1,7", @connection.add_limit_offset!("", :limit=>sql_inject)
assert_equal " LIMIT 7, 1", @connection.add_limit_offset!("", :limit=>sql_inject, :offset=>7)
else
assert_equal " LIMIT 1,7", @connection.add_limit_offset!("", :limit=>sql_inject)
assert_equal " LIMIT 1,7 OFFSET 7", @connection.add_limit_offset!("", :limit=>sql_inject, :offset=>7)
end
end
end

View file

@ -1,95 +0,0 @@
require "cases/helper"
require 'models/default'
require 'models/post'
require 'models/task'
class SqlServerAdapterTest < ActiveRecord::TestCase
class TableWithRealColumn < ActiveRecord::Base; end
fixtures :posts, :tasks
def setup
@connection = ActiveRecord::Base.connection
end
def teardown
@connection.execute("SET LANGUAGE us_english") rescue nil
end
def test_real_column_has_float_type
assert_equal :float, TableWithRealColumn.columns_hash["real_number"].type
end
# SQL Server 2000 has a bug where some unambiguous date formats are not
# correctly identified if the session language is set to german
def test_date_insertion_when_language_is_german
@connection.execute("SET LANGUAGE deutsch")
assert_nothing_raised do
Task.create(:starting => Time.utc(2000, 1, 31, 5, 42, 0), :ending => Date.new(2006, 12, 31))
end
end
def test_indexes_with_descending_order
# Make sure we have an index with descending order
@connection.execute "CREATE INDEX idx_credit_limit ON accounts (credit_limit DESC)" rescue nil
assert_equal ["credit_limit"], @connection.indexes('accounts').first.columns
ensure
@connection.execute "DROP INDEX accounts.idx_credit_limit"
end
def test_execute_without_block_closes_statement
assert_all_statements_used_are_closed do
@connection.execute("SELECT 1")
end
end
def test_execute_with_block_closes_statement
assert_all_statements_used_are_closed do
@connection.execute("SELECT 1") do |sth|
assert !sth.finished?, "Statement should still be alive within block"
end
end
end
def test_insert_with_identity_closes_statement
assert_all_statements_used_are_closed do
@connection.insert("INSERT INTO accounts ([id], [firm_id],[credit_limit]) values (999, 1, 50)")
end
end
def test_insert_without_identity_closes_statement
assert_all_statements_used_are_closed do
@connection.insert("INSERT INTO accounts ([firm_id],[credit_limit]) values (1, 50)")
end
end
def test_active_closes_statement
assert_all_statements_used_are_closed do
@connection.active?
end
end
def assert_all_statements_used_are_closed(&block)
existing_handles = []
ObjectSpace.each_object(DBI::StatementHandle) {|handle| existing_handles << handle}
GC.disable
yield
used_handles = []
ObjectSpace.each_object(DBI::StatementHandle) {|handle| used_handles << handle unless existing_handles.include? handle}
assert_block "No statements were used within given block" do
used_handles.size > 0
end
ObjectSpace.each_object(DBI::StatementHandle) do |handle|
assert_block "Statement should have been closed within given block" do
handle.finished?
end
end
ensure
GC.enable
end
end

View file

@ -1,128 +0,0 @@
require "cases/helper"
require 'models/customer'
class AggregationsTest < ActiveRecord::TestCase
fixtures :customers
def test_find_single_value_object
assert_equal 50, customers(:david).balance.amount
assert_kind_of Money, customers(:david).balance
assert_equal 300, customers(:david).balance.exchange_to("DKK").amount
end
def test_find_multiple_value_object
assert_equal customers(:david).address_street, customers(:david).address.street
assert(
customers(:david).address.close_to?(Address.new("Different Street", customers(:david).address_city, customers(:david).address_country))
)
end
def test_change_single_value_object
customers(:david).balance = Money.new(100)
customers(:david).save
assert_equal 100, customers(:david).reload.balance.amount
end
def test_immutable_value_objects
customers(:david).balance = Money.new(100)
assert_raise(ActiveSupport::FrozenObjectError) { customers(:david).balance.instance_eval { @amount = 20 } }
end
def test_inferred_mapping
assert_equal "35.544623640962634", customers(:david).gps_location.latitude
assert_equal "-105.9309951055148", customers(:david).gps_location.longitude
customers(:david).gps_location = GpsLocation.new("39x-110")
assert_equal "39", customers(:david).gps_location.latitude
assert_equal "-110", customers(:david).gps_location.longitude
customers(:david).save
customers(:david).reload
assert_equal "39", customers(:david).gps_location.latitude
assert_equal "-110", customers(:david).gps_location.longitude
end
def test_reloaded_instance_refreshes_aggregations
assert_equal "35.544623640962634", customers(:david).gps_location.latitude
assert_equal "-105.9309951055148", customers(:david).gps_location.longitude
Customer.update_all("gps_location = '24x113'")
customers(:david).reload
assert_equal '24x113', customers(:david)['gps_location']
assert_equal GpsLocation.new('24x113'), customers(:david).gps_location
end
def test_gps_equality
assert GpsLocation.new('39x110') == GpsLocation.new('39x110')
end
def test_gps_inequality
assert GpsLocation.new('39x110') != GpsLocation.new('39x111')
end
def test_allow_nil_gps_is_nil
assert_equal nil, customers(:zaphod).gps_location
end
def test_allow_nil_gps_set_to_nil
customers(:david).gps_location = nil
customers(:david).save
customers(:david).reload
assert_equal nil, customers(:david).gps_location
end
def test_allow_nil_set_address_attributes_to_nil
customers(:zaphod).address = nil
assert_equal nil, customers(:zaphod).attributes[:address_street]
assert_equal nil, customers(:zaphod).attributes[:address_city]
assert_equal nil, customers(:zaphod).attributes[:address_country]
end
def test_allow_nil_address_set_to_nil
customers(:zaphod).address = nil
customers(:zaphod).save
customers(:zaphod).reload
assert_equal nil, customers(:zaphod).address
end
def test_nil_raises_error_when_allow_nil_is_false
assert_raise(NoMethodError) { customers(:david).balance = nil }
end
def test_allow_nil_address_loaded_when_only_some_attributes_are_nil
customers(:zaphod).address_street = nil
customers(:zaphod).save
customers(:zaphod).reload
assert_kind_of Address, customers(:zaphod).address
assert customers(:zaphod).address.street.nil?
end
def test_nil_assignment_results_in_nil
customers(:david).gps_location = GpsLocation.new('39x111')
assert_not_equal nil, customers(:david).gps_location
customers(:david).gps_location = nil
assert_equal nil, customers(:david).gps_location
end
end
class OverridingAggregationsTest < ActiveRecord::TestCase
class Name; end
class DifferentName; end
class Person < ActiveRecord::Base
composed_of :composed_of, :mapping => %w(person_first_name first_name)
end
class DifferentPerson < Person
composed_of :composed_of, :class_name => 'DifferentName', :mapping => %w(different_person_first_name first_name)
end
def test_composed_of_aggregation_redefinition_reflections_should_differ_and_not_inherited
assert_not_equal Person.reflect_on_aggregation(:composed_of),
DifferentPerson.reflect_on_aggregation(:composed_of)
end
end

View file

@ -1,33 +0,0 @@
require "cases/helper"
require 'active_record/schema'
if ActiveRecord::Base.connection.supports_migrations?
class ActiveRecordSchemaTest < ActiveRecord::TestCase
self.use_transactional_fixtures = false
def setup
@connection = ActiveRecord::Base.connection
end
def teardown
@connection.drop_table :fruits rescue nil
end
def test_schema_define
ActiveRecord::Schema.define(:version => 7) do
create_table :fruits do |t|
t.column :color, :string
t.column :fruit_size, :string # NOTE: "size" is reserved in Oracle
t.column :texture, :string
t.column :flavor, :string
end
end
assert_nothing_raised { @connection.select_all "SELECT * FROM fruits" }
assert_nothing_raised { @connection.select_all "SELECT * FROM schema_migrations" }
assert_equal 7, ActiveRecord::Migrator::current_version
end
end
end

View file

@ -1,412 +0,0 @@
require "cases/helper"
require 'models/developer'
require 'models/project'
require 'models/company'
require 'models/topic'
require 'models/reply'
require 'models/computer'
require 'models/customer'
require 'models/order'
require 'models/post'
require 'models/author'
require 'models/tag'
require 'models/tagging'
require 'models/comment'
require 'models/sponsor'
require 'models/member'
class BelongsToAssociationsTest < ActiveRecord::TestCase
fixtures :accounts, :companies, :developers, :projects, :topics,
:developers_projects, :computers, :authors, :posts, :tags, :taggings, :comments
def test_belongs_to
Client.find(3).firm.name
assert_equal companies(:first_firm).name, Client.find(3).firm.name
assert !Client.find(3).firm.nil?, "Microsoft should have a firm"
end
def test_proxy_assignment
account = Account.find(1)
assert_nothing_raised { account.firm = account.firm }
end
def test_triple_equality
assert Client.find(3).firm === Firm
assert Firm === Client.find(3).firm
end
def test_type_mismatch
assert_raise(ActiveRecord::AssociationTypeMismatch) { Account.find(1).firm = 1 }
assert_raise(ActiveRecord::AssociationTypeMismatch) { Account.find(1).firm = Project.find(1) }
end
def test_natural_assignment
apple = Firm.create("name" => "Apple")
citibank = Account.create("credit_limit" => 10)
citibank.firm = apple
assert_equal apple.id, citibank.firm_id
end
def test_no_unexpected_aliasing
first_firm = companies(:first_firm)
another_firm = companies(:another_firm)
citibank = Account.create("credit_limit" => 10)
citibank.firm = first_firm
original_proxy = citibank.firm
citibank.firm = another_firm
assert_equal first_firm.object_id, original_proxy.target.object_id
assert_equal another_firm.object_id, citibank.firm.target.object_id
end
def test_creating_the_belonging_object
citibank = Account.create("credit_limit" => 10)
apple = citibank.create_firm("name" => "Apple")
assert_equal apple, citibank.firm
citibank.save
citibank.reload
assert_equal apple, citibank.firm
end
def test_building_the_belonging_object
citibank = Account.create("credit_limit" => 10)
apple = citibank.build_firm("name" => "Apple")
citibank.save
assert_equal apple.id, citibank.firm_id
end
def test_natural_assignment_to_nil
client = Client.find(3)
client.firm = nil
client.save
assert_nil client.firm(true)
assert_nil client.client_of
end
def test_with_different_class_name
assert_equal Company.find(1).name, Company.find(3).firm_with_other_name.name
assert_not_nil Company.find(3).firm_with_other_name, "Microsoft should have a firm"
end
def test_with_condition
assert_equal Company.find(1).name, Company.find(3).firm_with_condition.name
assert_not_nil Company.find(3).firm_with_condition, "Microsoft should have a firm"
end
def test_with_select
assert_equal Company.find(2).firm_with_select.attributes.size, 1
assert_equal Company.find(2, :include => :firm_with_select ).firm_with_select.attributes.size, 1
end
def test_belongs_to_counter
debate = Topic.create("title" => "debate")
assert_equal 0, debate.send(:read_attribute, "replies_count"), "No replies yet"
trash = debate.replies.create("title" => "blah!", "content" => "world around!")
assert_equal 1, Topic.find(debate.id).send(:read_attribute, "replies_count"), "First reply created"
trash.destroy
assert_equal 0, Topic.find(debate.id).send(:read_attribute, "replies_count"), "First reply deleted"
end
def test_belongs_to_counter_with_assigning_nil
p = Post.find(1)
c = Comment.find(1)
assert_equal p.id, c.post_id
assert_equal 2, Post.find(p.id).comments.size
c.post = nil
assert_equal 1, Post.find(p.id).comments.size
end
def test_belongs_to_counter_with_reassigning
t1 = Topic.create("title" => "t1")
t2 = Topic.create("title" => "t2")
r1 = Reply.new("title" => "r1", "content" => "r1")
r1.topic = t1
assert r1.save
assert_equal 1, Topic.find(t1.id).replies.size
assert_equal 0, Topic.find(t2.id).replies.size
r1.topic = Topic.find(t2.id)
assert r1.save
assert_equal 0, Topic.find(t1.id).replies.size
assert_equal 1, Topic.find(t2.id).replies.size
r1.topic = nil
assert_equal 0, Topic.find(t1.id).replies.size
assert_equal 0, Topic.find(t2.id).replies.size
r1.topic = t1
assert_equal 1, Topic.find(t1.id).replies.size
assert_equal 0, Topic.find(t2.id).replies.size
r1.destroy
assert_equal 0, Topic.find(t1.id).replies.size
assert_equal 0, Topic.find(t2.id).replies.size
end
def test_belongs_to_counter_after_save
topic = Topic.create!(:title => "monday night")
topic.replies.create!(:title => "re: monday night", :content => "football")
assert_equal 1, Topic.find(topic.id)[:replies_count]
topic.save!
assert_equal 1, Topic.find(topic.id)[:replies_count]
end
def test_belongs_to_counter_after_update_attributes
topic = Topic.create!(:title => "37s")
topic.replies.create!(:title => "re: 37s", :content => "rails")
assert_equal 1, Topic.find(topic.id)[:replies_count]
topic.update_attributes(:title => "37signals")
assert_equal 1, Topic.find(topic.id)[:replies_count]
end
def test_belongs_to_counter_after_save
topic = Topic.create("title" => "monday night")
topic.replies.create("title" => "re: monday night", "content" => "football")
assert_equal 1, Topic.find(topic.id).send(:read_attribute, "replies_count")
topic.save
assert_equal 1, Topic.find(topic.id).send(:read_attribute, "replies_count")
end
def test_belongs_to_counter_after_update_attributes
topic = Topic.create("title" => "37s")
topic.replies.create("title" => "re: 37s", "content" => "rails")
assert_equal 1, Topic.find(topic.id).send(:read_attribute, "replies_count")
topic.update_attributes("title" => "37signals")
assert_equal 1, Topic.find(topic.id).send(:read_attribute, "replies_count")
end
def test_assignment_before_parent_saved
client = Client.find(:first)
apple = Firm.new("name" => "Apple")
client.firm = apple
assert_equal apple, client.firm
assert apple.new_record?
assert client.save
assert apple.save
assert !apple.new_record?
assert_equal apple, client.firm
assert_equal apple, client.firm(true)
end
def test_assignment_before_child_saved
final_cut = Client.new("name" => "Final Cut")
firm = Firm.find(1)
final_cut.firm = firm
assert final_cut.new_record?
assert final_cut.save
assert !final_cut.new_record?
assert !firm.new_record?
assert_equal firm, final_cut.firm
assert_equal firm, final_cut.firm(true)
end
def test_assignment_before_either_saved
final_cut = Client.new("name" => "Final Cut")
apple = Firm.new("name" => "Apple")
final_cut.firm = apple
assert final_cut.new_record?
assert apple.new_record?
assert final_cut.save
assert !final_cut.new_record?
assert !apple.new_record?
assert_equal apple, final_cut.firm
assert_equal apple, final_cut.firm(true)
end
def test_new_record_with_foreign_key_but_no_object
c = Client.new("firm_id" => 1)
assert_equal Firm.find(:first), c.firm_with_basic_id
end
def test_forgetting_the_load_when_foreign_key_enters_late
c = Client.new
assert_nil c.firm_with_basic_id
c.firm_id = 1
assert_equal Firm.find(:first), c.firm_with_basic_id
end
def test_field_name_same_as_foreign_key
computer = Computer.find(1)
assert_not_nil computer.developer, ":foreign key == attribute didn't lock up" # '
end
def test_counter_cache
topic = Topic.create :title => "Zoom-zoom-zoom"
assert_equal 0, topic[:replies_count]
reply = Reply.create(:title => "re: zoom", :content => "speedy quick!")
reply.topic = topic
assert_equal 1, topic.reload[:replies_count]
assert_equal 1, topic.replies.size
topic[:replies_count] = 15
assert_equal 15, topic.replies.size
end
def test_custom_counter_cache
reply = Reply.create(:title => "re: zoom", :content => "speedy quick!")
assert_equal 0, reply[:replies_count]
silly = SillyReply.create(:title => "gaga", :content => "boo-boo")
silly.reply = reply
assert_equal 1, reply.reload[:replies_count]
assert_equal 1, reply.replies.size
reply[:replies_count] = 17
assert_equal 17, reply.replies.size
end
def test_store_two_association_with_one_save
num_orders = Order.count
num_customers = Customer.count
order = Order.new
customer1 = order.billing = Customer.new
customer2 = order.shipping = Customer.new
assert order.save
assert_equal customer1, order.billing
assert_equal customer2, order.shipping
order.reload
assert_equal customer1, order.billing
assert_equal customer2, order.shipping
assert_equal num_orders +1, Order.count
assert_equal num_customers +2, Customer.count
end
def test_store_association_in_two_relations_with_one_save
num_orders = Order.count
num_customers = Customer.count
order = Order.new
customer = order.billing = order.shipping = Customer.new
assert order.save
assert_equal customer, order.billing
assert_equal customer, order.shipping
order.reload
assert_equal customer, order.billing
assert_equal customer, order.shipping
assert_equal num_orders +1, Order.count
assert_equal num_customers +1, Customer.count
end
def test_store_association_in_two_relations_with_one_save_in_existing_object
num_orders = Order.count
num_customers = Customer.count
order = Order.create
customer = order.billing = order.shipping = Customer.new
assert order.save
assert_equal customer, order.billing
assert_equal customer, order.shipping
order.reload
assert_equal customer, order.billing
assert_equal customer, order.shipping
assert_equal num_orders +1, Order.count
assert_equal num_customers +1, Customer.count
end
def test_store_association_in_two_relations_with_one_save_in_existing_object_with_values
num_orders = Order.count
num_customers = Customer.count
order = Order.create
customer = order.billing = order.shipping = Customer.new
assert order.save
assert_equal customer, order.billing
assert_equal customer, order.shipping
order.reload
customer = order.billing = order.shipping = Customer.new
assert order.save
order.reload
assert_equal customer, order.billing
assert_equal customer, order.shipping
assert_equal num_orders +1, Order.count
assert_equal num_customers +2, Customer.count
end
def test_association_assignment_sticks
post = Post.find(:first)
author1, author2 = Author.find(:all, :limit => 2)
assert_not_nil author1
assert_not_nil author2
# make sure the association is loaded
post.author
# set the association by id, directly
post.author_id = author2.id
# save and reload
post.save!
post.reload
# the author id of the post should be the id we set
assert_equal post.author_id, author2.id
end
def test_cant_save_readonly_association
assert_raise(ActiveRecord::ReadOnlyRecord) { companies(:first_client).readonly_firm.save! }
assert companies(:first_client).readonly_firm.readonly?
end
def test_polymorphic_assignment_foreign_type_field_updating
# should update when assigning a saved record
sponsor = Sponsor.new
member = Member.create
sponsor.sponsorable = member
assert_equal "Member", sponsor.sponsorable_type
# should update when assigning a new record
sponsor = Sponsor.new
member = Member.new
sponsor.sponsorable = member
assert_equal "Member", sponsor.sponsorable_type
end
def test_polymorphic_assignment_updates_foreign_id_field_for_new_and_saved_records
sponsor = Sponsor.new
saved_member = Member.create
new_member = Member.new
sponsor.sponsorable = saved_member
assert_equal saved_member.id, sponsor.sponsorable_id
sponsor.sponsorable = new_member
assert_equal nil, sponsor.sponsorable_id
end
end

View file

@ -1,161 +0,0 @@
require "cases/helper"
require 'models/post'
require 'models/comment'
require 'models/author'
require 'models/category'
require 'models/project'
require 'models/developer'
class AssociationCallbacksTest < ActiveRecord::TestCase
fixtures :posts, :authors, :projects, :developers
def setup
@david = authors(:david)
@thinking = posts(:thinking)
@authorless = posts(:authorless)
assert @david.post_log.empty?
end
def test_adding_macro_callbacks
@david.posts_with_callbacks << @thinking
assert_equal ["before_adding#{@thinking.id}", "after_adding#{@thinking.id}"], @david.post_log
@david.posts_with_callbacks << @thinking
assert_equal ["before_adding#{@thinking.id}", "after_adding#{@thinking.id}", "before_adding#{@thinking.id}",
"after_adding#{@thinking.id}"], @david.post_log
end
def test_adding_with_proc_callbacks
@david.posts_with_proc_callbacks << @thinking
assert_equal ["before_adding#{@thinking.id}", "after_adding#{@thinking.id}"], @david.post_log
@david.posts_with_proc_callbacks << @thinking
assert_equal ["before_adding#{@thinking.id}", "after_adding#{@thinking.id}", "before_adding#{@thinking.id}",
"after_adding#{@thinking.id}"], @david.post_log
end
def test_removing_with_macro_callbacks
first_post, second_post = @david.posts_with_callbacks[0, 2]
@david.posts_with_callbacks.delete(first_post)
assert_equal ["before_removing#{first_post.id}", "after_removing#{first_post.id}"], @david.post_log
@david.posts_with_callbacks.delete(second_post)
assert_equal ["before_removing#{first_post.id}", "after_removing#{first_post.id}", "before_removing#{second_post.id}",
"after_removing#{second_post.id}"], @david.post_log
end
def test_removing_with_proc_callbacks
first_post, second_post = @david.posts_with_callbacks[0, 2]
@david.posts_with_proc_callbacks.delete(first_post)
assert_equal ["before_removing#{first_post.id}", "after_removing#{first_post.id}"], @david.post_log
@david.posts_with_proc_callbacks.delete(second_post)
assert_equal ["before_removing#{first_post.id}", "after_removing#{first_post.id}", "before_removing#{second_post.id}",
"after_removing#{second_post.id}"], @david.post_log
end
def test_multiple_callbacks
@david.posts_with_multiple_callbacks << @thinking
assert_equal ["before_adding#{@thinking.id}", "before_adding_proc#{@thinking.id}", "after_adding#{@thinking.id}",
"after_adding_proc#{@thinking.id}"], @david.post_log
@david.posts_with_multiple_callbacks << @thinking
assert_equal ["before_adding#{@thinking.id}", "before_adding_proc#{@thinking.id}", "after_adding#{@thinking.id}",
"after_adding_proc#{@thinking.id}", "before_adding#{@thinking.id}", "before_adding_proc#{@thinking.id}",
"after_adding#{@thinking.id}", "after_adding_proc#{@thinking.id}"], @david.post_log
end
def test_has_many_callbacks_with_create
morten = Author.create :name => "Morten"
post = morten.posts_with_proc_callbacks.create! :title => "Hello", :body => "How are you doing?"
assert_equal ["before_adding<new>", "after_adding#{post.id}"], morten.post_log
end
def test_has_many_callbacks_with_create!
morten = Author.create! :name => "Morten"
post = morten.posts_with_proc_callbacks.create :title => "Hello", :body => "How are you doing?"
assert_equal ["before_adding<new>", "after_adding#{post.id}"], morten.post_log
end
def test_has_many_callbacks_for_save_on_parent
jack = Author.new :name => "Jack"
post = jack.posts_with_callbacks.build :title => "Call me back!", :body => "Before you wake up and after you sleep"
callback_log = ["before_adding<new>", "after_adding#{jack.posts_with_callbacks.first.id}"]
assert_equal callback_log, jack.post_log
assert jack.save
assert_equal 1, jack.posts_with_callbacks.count
assert_equal callback_log, jack.post_log
end
def test_has_and_belongs_to_many_add_callback
david = developers(:david)
ar = projects(:active_record)
assert ar.developers_log.empty?
ar.developers_with_callbacks << david
assert_equal ["before_adding#{david.id}", "after_adding#{david.id}"], ar.developers_log
ar.developers_with_callbacks << david
assert_equal ["before_adding#{david.id}", "after_adding#{david.id}", "before_adding#{david.id}",
"after_adding#{david.id}"], ar.developers_log
end
def test_has_and_belongs_to_many_after_add_called_after_save
ar = projects(:active_record)
assert ar.developers_log.empty?
alice = Developer.new(:name => 'alice')
ar.developers_with_callbacks << alice
assert_equal"after_adding#{alice.id}", ar.developers_log.last
bob = ar.developers_with_callbacks.create(:name => 'bob')
assert_equal "after_adding#{bob.id}", ar.developers_log.last
ar.developers_with_callbacks.build(:name => 'charlie')
assert_equal "after_adding<new>", ar.developers_log.last
end
def test_has_and_belongs_to_many_remove_callback
david = developers(:david)
jamis = developers(:jamis)
activerecord = projects(:active_record)
assert activerecord.developers_log.empty?
activerecord.developers_with_callbacks.delete(david)
assert_equal ["before_removing#{david.id}", "after_removing#{david.id}"], activerecord.developers_log
activerecord.developers_with_callbacks.delete(jamis)
assert_equal ["before_removing#{david.id}", "after_removing#{david.id}", "before_removing#{jamis.id}",
"after_removing#{jamis.id}"], activerecord.developers_log
end
def test_has_and_belongs_to_many_remove_callback_on_clear
activerecord = projects(:active_record)
assert activerecord.developers_log.empty?
if activerecord.developers_with_callbacks.size == 0
activerecord.developers << developers(:david)
activerecord.developers << developers(:jamis)
activerecord.reload
assert activerecord.developers_with_callbacks.size == 2
end
log_array = activerecord.developers_with_callbacks.collect {|d| ["before_removing#{d.id}","after_removing#{d.id}"]}.flatten.sort
assert activerecord.developers_with_callbacks.clear
assert_equal log_array, activerecord.developers_log.sort
end
def test_has_many_and_belongs_to_many_callbacks_for_save_on_parent
project = Project.new :name => "Callbacks"
project.developers_with_callbacks.build :name => "Jack", :salary => 95000
callback_log = ["before_adding<new>", "after_adding<new>"]
assert_equal callback_log, project.developers_log
assert project.save
assert_equal 1, project.developers_with_callbacks.size
assert_equal callback_log, project.developers_log
end
def test_dont_add_if_before_callback_raises_exception
assert !@david.unchangable_posts.include?(@authorless)
begin
@david.unchangable_posts << @authorless
rescue Exception => e
end
assert @david.post_log.empty?
assert !@david.unchangable_posts.include?(@authorless)
@david.reload
assert !@david.unchangable_posts.include?(@authorless)
end
end

View file

@ -1,111 +0,0 @@
require "cases/helper"
require 'models/post'
require 'models/comment'
require 'models/author'
require 'models/category'
require 'models/categorization'
require 'models/company'
require 'models/topic'
require 'models/reply'
class CascadedEagerLoadingTest < ActiveRecord::TestCase
fixtures :authors, :mixins, :companies, :posts, :topics
def test_eager_association_loading_with_cascaded_two_levels
authors = Author.find(:all, :include=>{:posts=>:comments}, :order=>"authors.id")
assert_equal 2, authors.size
assert_equal 5, authors[0].posts.size
assert_equal 1, authors[1].posts.size
assert_equal 9, authors[0].posts.collect{|post| post.comments.size }.inject(0){|sum,i| sum+i}
end
def test_eager_association_loading_with_cascaded_two_levels_and_one_level
authors = Author.find(:all, :include=>[{:posts=>:comments}, :categorizations], :order=>"authors.id")
assert_equal 2, authors.size
assert_equal 5, authors[0].posts.size
assert_equal 1, authors[1].posts.size
assert_equal 9, authors[0].posts.collect{|post| post.comments.size }.inject(0){|sum,i| sum+i}
assert_equal 1, authors[0].categorizations.size
assert_equal 2, authors[1].categorizations.size
end
def test_eager_association_loading_with_cascaded_two_levels_with_two_has_many_associations
authors = Author.find(:all, :include=>{:posts=>[:comments, :categorizations]}, :order=>"authors.id")
assert_equal 2, authors.size
assert_equal 5, authors[0].posts.size
assert_equal 1, authors[1].posts.size
assert_equal 9, authors[0].posts.collect{|post| post.comments.size }.inject(0){|sum,i| sum+i}
end
def test_eager_association_loading_with_cascaded_two_levels_and_self_table_reference
authors = Author.find(:all, :include=>{:posts=>[:comments, :author]}, :order=>"authors.id")
assert_equal 2, authors.size
assert_equal 5, authors[0].posts.size
assert_equal authors(:david).name, authors[0].name
assert_equal [authors(:david).name], authors[0].posts.collect{|post| post.author.name}.uniq
end
def test_eager_association_loading_with_cascaded_two_levels_with_condition
authors = Author.find(:all, :include=>{:posts=>:comments}, :conditions=>"authors.id=1", :order=>"authors.id")
assert_equal 1, authors.size
assert_equal 5, authors[0].posts.size
end
def test_eager_association_loading_with_cascaded_three_levels_by_ping_pong
firms = Firm.find(:all, :include=>{:account=>{:firm=>:account}}, :order=>"companies.id")
assert_equal 2, firms.size
assert_equal firms.first.account, firms.first.account.firm.account
assert_equal companies(:first_firm).account, assert_no_queries { firms.first.account.firm.account }
assert_equal companies(:first_firm).account.firm.account, assert_no_queries { firms.first.account.firm.account }
end
def test_eager_association_loading_with_has_many_sti
topics = Topic.find(:all, :include => :replies, :order => 'topics.id')
first, second, = topics(:first).replies.size, topics(:second).replies.size
assert_no_queries do
assert_equal first, topics[0].replies.size
assert_equal second, topics[1].replies.size
end
end
def test_eager_association_loading_with_belongs_to_sti
replies = Reply.find(:all, :include => :topic, :order => 'topics.id')
assert replies.include?(topics(:second))
assert !replies.include?(topics(:first))
assert_equal topics(:first), assert_no_queries { replies.first.topic }
end
def test_eager_association_loading_with_multiple_stis_and_order
author = Author.find(:first, :include => { :posts => [ :special_comments , :very_special_comment ] }, :order => 'authors.name, comments.body, very_special_comments_posts.body', :conditions => 'posts.id = 4')
assert_equal authors(:david), author
assert_no_queries do
author.posts.first.special_comments
author.posts.first.very_special_comment
end
end
def test_eager_association_loading_of_stis_with_multiple_references
authors = Author.find(:all, :include => { :posts => { :special_comments => { :post => [ :special_comments, :very_special_comment ] } } }, :order => 'comments.body, very_special_comments_posts.body', :conditions => 'posts.id = 4')
assert_equal [authors(:david)], authors
assert_no_queries do
authors.first.posts.first.special_comments.first.post.special_comments
authors.first.posts.first.special_comments.first.post.very_special_comment
end
end
end
require 'models/vertex'
require 'models/edge'
class CascadedEagerLoadingTest < ActiveRecord::TestCase
fixtures :edges, :vertices
def test_eager_association_loading_with_recursive_cascading_four_levels_has_many_through
source = Vertex.find(:first, :include=>{:sinks=>{:sinks=>{:sinks=>:sinks}}}, :order => 'vertices.id')
assert_equal vertices(:vertex_4), assert_no_queries { source.sinks.first.sinks.first.sinks.first }
end
def test_eager_association_loading_with_recursive_cascading_four_levels_has_and_belongs_to_many
sink = Vertex.find(:first, :include=>{:sources=>{:sources=>{:sources=>:sources}}}, :order => 'vertices.id DESC')
assert_equal vertices(:vertex_1), assert_no_queries { sink.sources.first.sources.first.sources.first.sources.first }
end
end

View file

@ -1,83 +0,0 @@
require 'cases/helper'
class ShapeExpression < ActiveRecord::Base
belongs_to :shape, :polymorphic => true
belongs_to :paint, :polymorphic => true
end
class Circle < ActiveRecord::Base
has_many :shape_expressions, :as => :shape
end
class Square < ActiveRecord::Base
has_many :shape_expressions, :as => :shape
end
class Triangle < ActiveRecord::Base
has_many :shape_expressions, :as => :shape
end
class PaintColor < ActiveRecord::Base
has_many :shape_expressions, :as => :paint
belongs_to :non_poly, :foreign_key => "non_poly_one_id", :class_name => "NonPolyOne"
end
class PaintTexture < ActiveRecord::Base
has_many :shape_expressions, :as => :paint
belongs_to :non_poly, :foreign_key => "non_poly_two_id", :class_name => "NonPolyTwo"
end
class NonPolyOne < ActiveRecord::Base
has_many :paint_colors
end
class NonPolyTwo < ActiveRecord::Base
has_many :paint_textures
end
class EagerLoadPolyAssocsTest < ActiveRecord::TestCase
NUM_SIMPLE_OBJS = 50
NUM_SHAPE_EXPRESSIONS = 100
def setup
generate_test_object_graphs
end
def teardown
[Circle, Square, Triangle, PaintColor, PaintTexture,
ShapeExpression, NonPolyOne, NonPolyTwo].each do |c|
c.delete_all
end
end
# meant to be supplied as an ID, never returns 0
def rand_simple
val = (NUM_SIMPLE_OBJS * rand).round
val == 0 ? 1 : val
end
def generate_test_object_graphs
1.upto(NUM_SIMPLE_OBJS) do
[Circle, Square, Triangle, NonPolyOne, NonPolyTwo].map(&:create!)
end
1.upto(NUM_SIMPLE_OBJS) do |i|
PaintColor.create!(:non_poly_one_id => rand_simple)
PaintTexture.create!(:non_poly_two_id => rand_simple)
end
1.upto(NUM_SHAPE_EXPRESSIONS) do |i|
ShapeExpression.create!(:shape_type => [Circle, Square, Triangle].rand.to_s, :shape_id => rand_simple,
:paint_type => [PaintColor, PaintTexture].rand.to_s, :paint_id => rand_simple)
end
end
def test_include_query
res = 0
res = ShapeExpression.find :all, :include => [ :shape, { :paint => :non_poly } ]
assert_equal NUM_SHAPE_EXPRESSIONS, res.size
assert_queries(0) do
res.each do |se|
assert_not_nil se.paint.non_poly, "this is the association that was loading incorrectly before the change"
assert_not_nil se.shape, "just making sure other associations still work"
end
end
end
end

View file

@ -1,145 +0,0 @@
require "cases/helper"
class Virus < ActiveRecord::Base
belongs_to :octopus
end
class Octopus < ActiveRecord::Base
has_one :virus
end
class Pass < ActiveRecord::Base
belongs_to :bus
end
class Bus < ActiveRecord::Base
has_many :passes
end
class Mess < ActiveRecord::Base
has_and_belongs_to_many :crises
end
class Crisis < ActiveRecord::Base
has_and_belongs_to_many :messes
has_many :analyses, :dependent => :destroy
has_many :successes, :through => :analyses
has_many :dresses, :dependent => :destroy
has_many :compresses, :through => :dresses
end
class Analysis < ActiveRecord::Base
belongs_to :crisis
belongs_to :success
end
class Success < ActiveRecord::Base
has_many :analyses, :dependent => :destroy
has_many :crises, :through => :analyses
end
class Dress < ActiveRecord::Base
belongs_to :crisis
has_many :compresses
end
class Compress < ActiveRecord::Base
belongs_to :dress
end
class EagerSingularizationTest < ActiveRecord::TestCase
def setup
if ActiveRecord::Base.connection.supports_migrations?
ActiveRecord::Base.connection.create_table :viri do |t|
t.column :octopus_id, :integer
t.column :species, :string
end
ActiveRecord::Base.connection.create_table :octopi do |t|
t.column :species, :string
end
ActiveRecord::Base.connection.create_table :passes do |t|
t.column :bus_id, :integer
t.column :rides, :integer
end
ActiveRecord::Base.connection.create_table :buses do |t|
t.column :name, :string
end
ActiveRecord::Base.connection.create_table :crises_messes, :id => false do |t|
t.column :crisis_id, :integer
t.column :mess_id, :integer
end
ActiveRecord::Base.connection.create_table :messes do |t|
t.column :name, :string
end
ActiveRecord::Base.connection.create_table :crises do |t|
t.column :name, :string
end
ActiveRecord::Base.connection.create_table :successes do |t|
t.column :name, :string
end
ActiveRecord::Base.connection.create_table :analyses do |t|
t.column :crisis_id, :integer
t.column :success_id, :integer
end
ActiveRecord::Base.connection.create_table :dresses do |t|
t.column :crisis_id, :integer
end
ActiveRecord::Base.connection.create_table :compresses do |t|
t.column :dress_id, :integer
end
@have_tables = true
else
@have_tables = false
end
end
def teardown
ActiveRecord::Base.connection.drop_table :viri
ActiveRecord::Base.connection.drop_table :octopi
ActiveRecord::Base.connection.drop_table :passes
ActiveRecord::Base.connection.drop_table :buses
ActiveRecord::Base.connection.drop_table :crises_messes
ActiveRecord::Base.connection.drop_table :messes
ActiveRecord::Base.connection.drop_table :crises
ActiveRecord::Base.connection.drop_table :successes
ActiveRecord::Base.connection.drop_table :analyses
ActiveRecord::Base.connection.drop_table :dresses
ActiveRecord::Base.connection.drop_table :compresses
end
def test_eager_no_extra_singularization_belongs_to
return unless @have_tables
assert_nothing_raised do
Virus.find(:all, :include => :octopus)
end
end
def test_eager_no_extra_singularization_has_one
return unless @have_tables
assert_nothing_raised do
Octopus.find(:all, :include => :virus)
end
end
def test_eager_no_extra_singularization_has_many
return unless @have_tables
assert_nothing_raised do
Bus.find(:all, :include => :passes)
end
end
def test_eager_no_extra_singularization_has_and_belongs_to_many
return unless @have_tables
assert_nothing_raised do
Crisis.find(:all, :include => :messes)
Mess.find(:all, :include => :crises)
end
end
def test_eager_no_extra_singularization_has_many_through_belongs_to
return unless @have_tables
assert_nothing_raised do
Crisis.find(:all, :include => :successes)
end
end
def test_eager_no_extra_singularization_has_many_through_has_many
return unless @have_tables
assert_nothing_raised do
Crisis.find(:all, :include => :compresses)
end
end
end

View file

@ -1,612 +0,0 @@
require "cases/helper"
require 'models/post'
require 'models/tagging'
require 'models/comment'
require 'models/author'
require 'models/category'
require 'models/company'
require 'models/person'
require 'models/reader'
require 'models/owner'
require 'models/pet'
require 'models/reference'
require 'models/job'
require 'models/subscriber'
require 'models/subscription'
require 'models/book'
class EagerAssociationTest < ActiveRecord::TestCase
fixtures :posts, :comments, :authors, :categories, :categories_posts,
:companies, :accounts, :tags, :taggings, :people, :readers,
:owners, :pets, :author_favorites, :jobs, :references, :subscribers, :subscriptions, :books
def test_loading_with_one_association
posts = Post.find(:all, :include => :comments)
post = posts.find { |p| p.id == 1 }
assert_equal 2, post.comments.size
assert post.comments.include?(comments(:greetings))
post = Post.find(:first, :include => :comments, :conditions => "posts.title = 'Welcome to the weblog'")
assert_equal 2, post.comments.size
assert post.comments.include?(comments(:greetings))
posts = Post.find(:all, :include => :last_comment)
post = posts.find { |p| p.id == 1 }
assert_equal Post.find(1).last_comment, post.last_comment
end
def test_loading_conditions_with_or
posts = authors(:david).posts.find(:all, :include => :comments, :conditions => "comments.body like 'Normal%' OR comments.#{QUOTED_TYPE} = 'SpecialComment'")
assert_nil posts.detect { |p| p.author_id != authors(:david).id },
"expected to find only david's posts"
end
def test_with_ordering
list = Post.find(:all, :include => :comments, :order => "posts.id DESC")
[:eager_other, :sti_habtm, :sti_post_and_comments, :sti_comments,
:authorless, :thinking, :welcome
].each_with_index do |post, index|
assert_equal posts(post), list[index]
end
end
def test_with_two_tables_in_from_without_getting_double_quoted
posts = Post.find(:all,
:select => "posts.*",
:from => "authors, posts",
:include => :comments,
:conditions => "posts.author_id = authors.id",
:order => "posts.id"
)
assert_equal 2, posts.first.comments.size
end
def test_loading_with_multiple_associations
posts = Post.find(:all, :include => [ :comments, :author, :categories ], :order => "posts.id")
assert_equal 2, posts.first.comments.size
assert_equal 2, posts.first.categories.size
assert posts.first.comments.include?(comments(:greetings))
end
def test_duplicate_middle_objects
comments = Comment.find :all, :conditions => 'post_id = 1', :include => [:post => :author]
assert_no_queries do
comments.each {|comment| comment.post.author.name}
end
end
def test_including_duplicate_objects_from_belongs_to
popular_post = Post.create!(:title => 'foo', :body => "I like cars!")
comment = popular_post.comments.create!(:body => "lol")
popular_post.readers.create!(:person => people(:michael))
popular_post.readers.create!(:person => people(:david))
readers = Reader.find(:all, :conditions => ["post_id = ?", popular_post.id],
:include => {:post => :comments})
readers.each do |reader|
assert_equal [comment], reader.post.comments
end
end
def test_including_duplicate_objects_from_has_many
car_post = Post.create!(:title => 'foo', :body => "I like cars!")
car_post.categories << categories(:general)
car_post.categories << categories(:technology)
comment = car_post.comments.create!(:body => "hmm")
categories = Category.find(:all, :conditions => ["posts.id=?", car_post.id],
:include => {:posts => :comments})
categories.each do |category|
assert_equal [comment], category.posts[0].comments
end
end
def test_loading_from_an_association
posts = authors(:david).posts.find(:all, :include => :comments, :order => "posts.id")
assert_equal 2, posts.first.comments.size
end
def test_loading_with_no_associations
assert_nil Post.find(posts(:authorless).id, :include => :author).author
end
def test_nested_loading_with_no_associations
assert_nothing_raised do
Post.find(posts(:authorless).id, :include => {:author => :author_addresss})
end
end
def test_eager_association_loading_with_belongs_to_and_foreign_keys
pets = Pet.find(:all, :include => :owner)
assert_equal 3, pets.length
end
def test_eager_association_loading_with_belongs_to
comments = Comment.find(:all, :include => :post)
assert_equal 10, comments.length
titles = comments.map { |c| c.post.title }
assert titles.include?(posts(:welcome).title)
assert titles.include?(posts(:sti_post_and_comments).title)
end
def test_eager_association_loading_with_belongs_to_and_limit
comments = Comment.find(:all, :include => :post, :limit => 5, :order => 'comments.id')
assert_equal 5, comments.length
assert_equal [1,2,3,5,6], comments.collect { |c| c.id }
end
def test_eager_association_loading_with_belongs_to_and_limit_and_conditions
comments = Comment.find(:all, :include => :post, :conditions => 'post_id = 4', :limit => 3, :order => 'comments.id')
assert_equal 3, comments.length
assert_equal [5,6,7], comments.collect { |c| c.id }
end
def test_eager_association_loading_with_belongs_to_and_limit_and_offset
comments = Comment.find(:all, :include => :post, :limit => 3, :offset => 2, :order => 'comments.id')
assert_equal 3, comments.length
assert_equal [3,5,6], comments.collect { |c| c.id }
end
def test_eager_association_loading_with_belongs_to_and_limit_and_offset_and_conditions
comments = Comment.find(:all, :include => :post, :conditions => 'post_id = 4', :limit => 3, :offset => 1, :order => 'comments.id')
assert_equal 3, comments.length
assert_equal [6,7,8], comments.collect { |c| c.id }
end
def test_eager_association_loading_with_belongs_to_and_limit_and_offset_and_conditions_array
comments = Comment.find(:all, :include => :post, :conditions => ['post_id = ?',4], :limit => 3, :offset => 1, :order => 'comments.id')
assert_equal 3, comments.length
assert_equal [6,7,8], comments.collect { |c| c.id }
end
def test_eager_association_loading_with_belongs_to_and_conditions_string_with_unquoted_table_name
assert_nothing_raised do
Comment.find(:all, :include => :post, :conditions => ['posts.id = ?',4])
end
end
def test_eager_association_loading_with_belongs_to_and_conditions_string_with_quoted_table_name
quoted_posts_id= Comment.connection.quote_table_name('posts') + '.' + Comment.connection.quote_column_name('id')
assert_nothing_raised do
Comment.find(:all, :include => :post, :conditions => ["#{quoted_posts_id} = ?",4])
end
end
def test_eager_association_loading_with_belongs_to_and_order_string_with_unquoted_table_name
assert_nothing_raised do
Comment.find(:all, :include => :post, :order => 'posts.id')
end
end
def test_eager_association_loading_with_belongs_to_and_order_string_with_quoted_table_name
quoted_posts_id= Comment.connection.quote_table_name('posts') + '.' + Comment.connection.quote_column_name('id')
assert_nothing_raised do
Comment.find(:all, :include => :post, :order => quoted_posts_id)
end
end
def test_eager_association_loading_with_belongs_to_and_limit_and_multiple_associations
posts = Post.find(:all, :include => [:author, :very_special_comment], :limit => 1, :order => 'posts.id')
assert_equal 1, posts.length
assert_equal [1], posts.collect { |p| p.id }
end
def test_eager_association_loading_with_belongs_to_and_limit_and_offset_and_multiple_associations
posts = Post.find(:all, :include => [:author, :very_special_comment], :limit => 1, :offset => 1, :order => 'posts.id')
assert_equal 1, posts.length
assert_equal [2], posts.collect { |p| p.id }
end
def test_eager_association_loading_with_belongs_to_inferred_foreign_key_from_association_name
author_favorite = AuthorFavorite.find(:first, :include => :favorite_author)
assert_equal authors(:mary), assert_no_queries { author_favorite.favorite_author }
end
def test_eager_load_belongs_to_quotes_table_and_column_names
job = Job.find jobs(:unicyclist).id, :include => :ideal_reference
references(:michael_unicyclist)
assert_no_queries{ assert_equal references(:michael_unicyclist), job.ideal_reference}
end
def test_eager_load_has_one_quotes_table_and_column_names
michael = Person.find(people(:michael), :include => :favourite_reference)
references(:michael_unicyclist)
assert_no_queries{ assert_equal references(:michael_unicyclist), michael.favourite_reference}
end
def test_eager_load_has_many_quotes_table_and_column_names
michael = Person.find(people(:michael), :include => :references)
references(:michael_magician,:michael_unicyclist)
assert_no_queries{ assert_equal references(:michael_magician,:michael_unicyclist), michael.references.sort_by(&:id) }
end
def test_eager_load_has_many_through_quotes_table_and_column_names
michael = Person.find(people(:michael), :include => :jobs)
jobs(:magician, :unicyclist)
assert_no_queries{ assert_equal jobs(:unicyclist, :magician), michael.jobs.sort_by(&:id) }
end
def test_eager_load_has_many_with_string_keys
subscriptions = subscriptions(:webster_awdr, :webster_rfr)
subscriber =Subscriber.find(subscribers(:second).id, :include => :subscriptions)
assert_equal subscriptions, subscriber.subscriptions.sort_by(&:id)
end
def test_eager_load_has_many_through_with_string_keys
books = books(:awdr, :rfr)
subscriber = Subscriber.find(subscribers(:second).id, :include => :books)
assert_equal books, subscriber.books.sort_by(&:id)
end
def test_eager_load_belongs_to_with_string_keys
subscriber = subscribers(:second)
subscription = Subscription.find(subscriptions(:webster_awdr).id, :include => :subscriber)
assert_equal subscriber, subscription.subscriber
end
def test_eager_association_loading_with_explicit_join
posts = Post.find(:all, :include => :comments, :joins => "INNER JOIN authors ON posts.author_id = authors.id AND authors.name = 'Mary'", :limit => 1, :order => 'author_id')
assert_equal 1, posts.length
end
def test_eager_with_has_many_through
posts_with_comments = people(:michael).posts.find(:all, :include => :comments)
posts_with_author = people(:michael).posts.find(:all, :include => :author )
posts_with_comments_and_author = people(:michael).posts.find(:all, :include => [ :comments, :author ])
assert_equal 2, posts_with_comments.inject(0) { |sum, post| sum += post.comments.size }
assert_equal authors(:david), assert_no_queries { posts_with_author.first.author }
assert_equal authors(:david), assert_no_queries { posts_with_comments_and_author.first.author }
end
def test_eager_with_has_many_through_an_sti_join_model
author = Author.find(:first, :include => :special_post_comments, :order => 'authors.id')
assert_equal [comments(:does_it_hurt)], assert_no_queries { author.special_post_comments }
end
def test_eager_with_has_many_through_an_sti_join_model_with_conditions_on_both
author = Author.find(:first, :include => :special_nonexistant_post_comments, :order => 'authors.id')
assert_equal [], author.special_nonexistant_post_comments
end
def test_eager_with_has_many_through_join_model_with_conditions
assert_equal Author.find(:first, :include => :hello_post_comments,
:order => 'authors.id').hello_post_comments.sort_by(&:id),
Author.find(:first, :order => 'authors.id').hello_post_comments.sort_by(&:id)
end
def test_eager_with_has_many_through_join_model_with_conditions_on_top_level
assert_equal comments(:more_greetings), Author.find(authors(:david).id, :include => :comments_with_order_and_conditions).comments_with_order_and_conditions.first
end
def test_eager_with_has_many_through_join_model_with_include
author_comments = Author.find(authors(:david).id, :include => :comments_with_include).comments_with_include.to_a
assert_no_queries do
author_comments.first.post.title
end
end
def test_eager_with_has_many_and_limit
posts = Post.find(:all, :order => 'posts.id asc', :include => [ :author, :comments ], :limit => 2)
assert_equal 2, posts.size
assert_equal 3, posts.inject(0) { |sum, post| sum += post.comments.size }
end
def test_eager_with_has_many_and_limit_and_conditions
if current_adapter?(:OpenBaseAdapter)
posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :conditions => "FETCHBLOB(posts.body) = 'hello'", :order => "posts.id")
else
posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :conditions => "posts.body = 'hello'", :order => "posts.id")
end
assert_equal 2, posts.size
assert_equal [4,5], posts.collect { |p| p.id }
end
def test_eager_with_has_many_and_limit_and_conditions_array
if current_adapter?(:OpenBaseAdapter)
posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :conditions => [ "FETCHBLOB(posts.body) = ?", 'hello' ], :order => "posts.id")
else
posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :conditions => [ "posts.body = ?", 'hello' ], :order => "posts.id")
end
assert_equal 2, posts.size
assert_equal [4,5], posts.collect { |p| p.id }
end
def test_eager_with_has_many_and_limit_and_conditions_array_on_the_eagers
posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :conditions => [ "authors.name = ?", 'David' ])
assert_equal 2, posts.size
count = Post.count(:include => [ :author, :comments ], :limit => 2, :conditions => [ "authors.name = ?", 'David' ])
assert_equal count, posts.size
end
def test_eager_with_has_many_and_limit_ond_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
posts = Post.count(:all, :include => [ :author, :comments ], :limit => 2, :offset => 10, :conditions => [ "authors.name = ?", 'David' ])
assert_equal 0, posts
end
def test_eager_with_has_many_and_limit_with_no_results
posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :conditions => "posts.title = 'magic forest'")
assert_equal 0, posts.size
end
def test_eager_count_performed_on_a_has_many_association_with_multi_table_conditional
author = authors(:david)
author_posts_without_comments = author.posts.select { |post| post.comments.blank? }
assert_equal author_posts_without_comments.size, author.posts.count(:all, :include => :comments, :conditions => 'comments.id is null')
end
def test_eager_count_performed_on_a_has_many_through_association_with_multi_table_conditional
person = people(:michael)
person_posts_without_comments = person.posts.select { |post| post.comments.blank? }
assert_equal person_posts_without_comments.size, person.posts_with_no_comments.count
end
def test_eager_with_has_and_belongs_to_many_and_limit
posts = Post.find(:all, :include => :categories, :order => "posts.id", :limit => 3)
assert_equal 3, posts.size
assert_equal 2, posts[0].categories.size
assert_equal 1, posts[1].categories.size
assert_equal 0, posts[2].categories.size
assert posts[0].categories.include?(categories(:technology))
assert posts[1].categories.include?(categories(:general))
end
def test_eager_with_has_many_and_limit_and_conditions_on_the_eagers
posts = authors(:david).posts.find(:all,
:include => :comments,
:conditions => "comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment'",
:limit => 2
)
assert_equal 2, posts.size
count = Post.count(
:include => [ :comments, :author ],
:conditions => "authors.name = 'David' AND (comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment')",
:limit => 2
)
assert_equal count, posts.size
end
def test_eager_with_has_many_and_limit_and_scoped_conditions_on_the_eagers
posts = nil
Post.with_scope(:find => {
:include => :comments,
:conditions => "comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment'"
}) do
posts = authors(:david).posts.find(:all, :limit => 2)
assert_equal 2, posts.size
end
Post.with_scope(:find => {
:include => [ :comments, :author ],
:conditions => "authors.name = 'David' AND (comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment')"
}) do
count = Post.count(:limit => 2)
assert_equal count, posts.size
end
end
def test_eager_with_has_many_and_limit_and_scoped_and_explicit_conditions_on_the_eagers
Post.with_scope(:find => { :conditions => "1=1" }) do
posts = authors(:david).posts.find(:all,
:include => :comments,
:conditions => "comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment'",
:limit => 2
)
assert_equal 2, posts.size
count = Post.count(
:include => [ :comments, :author ],
:conditions => "authors.name = 'David' AND (comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment')",
:limit => 2
)
assert_equal count, posts.size
end
end
def test_eager_with_scoped_order_using_association_limiting_without_explicit_scope
posts_with_explicit_order = Post.find(:all, :conditions => 'comments.id is not null', :include => :comments, :order => 'posts.id DESC', :limit => 2)
posts_with_scoped_order = Post.with_scope(:find => {:order => 'posts.id DESC'}) do
Post.find(:all, :conditions => 'comments.id is not null', :include => :comments, :limit => 2)
end
assert_equal posts_with_explicit_order, posts_with_scoped_order
end
def test_eager_association_loading_with_habtm
posts = Post.find(:all, :include => :categories, :order => "posts.id")
assert_equal 2, posts[0].categories.size
assert_equal 1, posts[1].categories.size
assert_equal 0, posts[2].categories.size
assert posts[0].categories.include?(categories(:technology))
assert posts[1].categories.include?(categories(:general))
end
def test_eager_with_inheritance
posts = SpecialPost.find(:all, :include => [ :comments ])
end
def test_eager_has_one_with_association_inheritance
post = Post.find(4, :include => [ :very_special_comment ])
assert_equal "VerySpecialComment", post.very_special_comment.class.to_s
end
def test_eager_has_many_with_association_inheritance
post = Post.find(4, :include => [ :special_comments ])
post.special_comments.each do |special_comment|
assert_equal "SpecialComment", special_comment.class.to_s
end
end
def test_eager_habtm_with_association_inheritance
post = Post.find(6, :include => [ :special_categories ])
assert_equal 1, post.special_categories.size
post.special_categories.each do |special_category|
assert_equal "SpecialCategory", special_category.class.to_s
end
end
def test_eager_with_has_one_dependent_does_not_destroy_dependent
assert_not_nil companies(:first_firm).account
f = Firm.find(:first, :include => :account,
:conditions => ["companies.name = ?", "37signals"])
assert_not_nil f.account
assert_equal companies(:first_firm, :reload).account, f.account
end
def test_eager_with_multi_table_conditional_properly_counts_the_records_when_using_size
author = authors(:david)
posts_with_no_comments = author.posts.select { |post| post.comments.blank? }
assert_equal posts_with_no_comments.size, author.posts_with_no_comments.size
assert_equal posts_with_no_comments, author.posts_with_no_comments
end
def test_eager_with_invalid_association_reference
assert_raises(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") {
post = Post.find(6, :include=> :monkeys )
}
assert_raises(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") {
post = Post.find(6, :include=>[ :monkeys ])
}
assert_raises(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") {
post = Post.find(6, :include=>[ 'monkeys' ])
}
assert_raises(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys, :elephants") {
post = Post.find(6, :include=>[ :monkeys, :elephants ])
}
end
def find_all_ordered(className, include=nil)
className.find(:all, :order=>"#{className.table_name}.#{className.primary_key}", :include=>include)
end
def test_limited_eager_with_order
assert_equal posts(:thinking, :sti_comments), Post.find(:all, :include => [:author, :comments], :conditions => "authors.name = 'David'", :order => 'UPPER(posts.title)', :limit => 2, :offset => 1)
assert_equal posts(:sti_post_and_comments, :sti_comments), Post.find(:all, :include => [:author, :comments], :conditions => "authors.name = 'David'", :order => 'UPPER(posts.title) DESC', :limit => 2, :offset => 1)
end
def test_limited_eager_with_multiple_order_columns
assert_equal posts(:thinking, :sti_comments), Post.find(:all, :include => [:author, :comments], :conditions => "authors.name = 'David'", :order => 'UPPER(posts.title), posts.id', :limit => 2, :offset => 1)
assert_equal posts(:sti_post_and_comments, :sti_comments), Post.find(:all, :include => [:author, :comments], :conditions => "authors.name = 'David'", :order => 'UPPER(posts.title) DESC, posts.id', :limit => 2, :offset => 1)
end
def test_preload_with_interpolation
assert_equal [comments(:greetings)], Post.find(posts(:welcome).id, :include => :comments_with_interpolated_conditions).comments_with_interpolated_conditions
end
def test_polymorphic_type_condition
post = Post.find(posts(:thinking).id, :include => :taggings)
assert post.taggings.include?(taggings(:thinking_general))
post = SpecialPost.find(posts(:thinking).id, :include => :taggings)
assert post.taggings.include?(taggings(:thinking_general))
end
def test_eager_with_multiple_associations_with_same_table_has_many_and_habtm
# Eager includes of has many and habtm associations aren't necessarily sorted in the same way
def assert_equal_after_sort(item1, item2, item3 = nil)
assert_equal(item1.sort{|a,b| a.id <=> b.id}, item2.sort{|a,b| a.id <=> b.id})
assert_equal(item3.sort{|a,b| a.id <=> b.id}, item2.sort{|a,b| a.id <=> b.id}) if item3
end
# Test regular association, association with conditions, association with
# STI, and association with conditions assured not to be true
post_types = [:posts, :other_posts, :special_posts]
# test both has_many and has_and_belongs_to_many
[Author, Category].each do |className|
d1 = find_all_ordered(className)
# test including all post types at once
d2 = find_all_ordered(className, post_types)
d1.each_index do |i|
assert_equal(d1[i], d2[i])
assert_equal_after_sort(d1[i].posts, d2[i].posts)
post_types[1..-1].each do |post_type|
# test including post_types together
d3 = find_all_ordered(className, [:posts, post_type])
assert_equal(d1[i], d3[i])
assert_equal_after_sort(d1[i].posts, d3[i].posts)
assert_equal_after_sort(d1[i].send(post_type), d2[i].send(post_type), d3[i].send(post_type))
end
end
end
end
def test_eager_with_multiple_associations_with_same_table_has_one
d1 = find_all_ordered(Firm)
d2 = find_all_ordered(Firm, :account)
d1.each_index do |i|
assert_equal(d1[i], d2[i])
assert_equal(d1[i].account, d2[i].account)
end
end
def test_eager_with_multiple_associations_with_same_table_belongs_to
firm_types = [:firm, :firm_with_basic_id, :firm_with_other_name, :firm_with_condition]
d1 = find_all_ordered(Client)
d2 = find_all_ordered(Client, firm_types)
d1.each_index do |i|
assert_equal(d1[i], d2[i])
firm_types.each { |type| assert_equal(d1[i].send(type), d2[i].send(type)) }
end
end
def test_eager_with_valid_association_as_string_not_symbol
assert_nothing_raised { Post.find(:all, :include => 'comments') }
end
def test_preconfigured_includes_with_belongs_to
author = posts(:welcome).author_with_posts
assert_no_queries {assert_equal 5, author.posts.size}
end
def test_preconfigured_includes_with_has_one
comment = posts(:sti_comments).very_special_comment_with_post
assert_no_queries {assert_equal posts(:sti_comments), comment.post}
end
def test_preconfigured_includes_with_has_many
posts = authors(:david).posts_with_comments
one = posts.detect { |p| p.id == 1 }
assert_no_queries do
assert_equal 5, posts.size
assert_equal 2, one.comments.size
end
end
def test_preconfigured_includes_with_habtm
posts = authors(:david).posts_with_categories
one = posts.detect { |p| p.id == 1 }
assert_no_queries do
assert_equal 5, posts.size
assert_equal 2, one.categories.size
end
end
def test_preconfigured_includes_with_has_many_and_habtm
posts = authors(:david).posts_with_comments_and_categories
one = posts.detect { |p| p.id == 1 }
assert_no_queries do
assert_equal 5, posts.size
assert_equal 2, one.comments.size
assert_equal 2, one.categories.size
end
end
def test_count_with_include
if current_adapter?(:SQLServerAdapter, :SybaseAdapter)
assert_equal 3, authors(:david).posts_with_comments.count(:conditions => "len(comments.body) > 15")
elsif current_adapter?(:OpenBaseAdapter)
assert_equal 3, authors(:david).posts_with_comments.count(:conditions => "length(FETCHBLOB(comments.body)) > 15")
else
assert_equal 3, authors(:david).posts_with_comments.count(:conditions => "length(comments.body) > 15")
end
end
def test_load_with_sti_sharing_association
assert_queries(2) do #should not do 1 query per subclass
Comment.find :all, :include => :post
end
end
end

View file

@ -1,47 +0,0 @@
require "cases/helper"
require 'models/post'
require 'models/comment'
require 'models/project'
require 'models/developer'
class AssociationsExtensionsTest < ActiveRecord::TestCase
fixtures :projects, :developers, :developers_projects, :comments, :posts
def test_extension_on_has_many
assert_equal comments(:more_greetings), posts(:welcome).comments.find_most_recent
end
def test_extension_on_habtm
assert_equal projects(:action_controller), developers(:david).projects.find_most_recent
end
def test_named_extension_on_habtm
assert_equal projects(:action_controller), developers(:david).projects_extended_by_name.find_most_recent
end
def test_named_two_extensions_on_habtm
assert_equal projects(:action_controller), developers(:david).projects_extended_by_name_twice.find_most_recent
assert_equal projects(:active_record), developers(:david).projects_extended_by_name_twice.find_least_recent
end
def test_named_extension_and_block_on_habtm
assert_equal projects(:action_controller), developers(:david).projects_extended_by_name_and_block.find_most_recent
assert_equal projects(:active_record), developers(:david).projects_extended_by_name_and_block.find_least_recent
end
def test_marshalling_extensions
david = developers(:david)
assert_equal projects(:action_controller), david.projects.find_most_recent
david = Marshal.load(Marshal.dump(david))
assert_equal projects(:action_controller), david.projects.find_most_recent
end
def test_marshalling_named_extensions
david = developers(:david)
assert_equal projects(:action_controller), david.projects_extended_by_name.find_most_recent
david = Marshal.load(Marshal.dump(david))
assert_equal projects(:action_controller), david.projects_extended_by_name.find_most_recent
end
end

View file

@ -1,684 +0,0 @@
require "cases/helper"
require 'models/developer'
require 'models/project'
require 'models/company'
require 'models/topic'
require 'models/reply'
require 'models/computer'
require 'models/customer'
require 'models/order'
require 'models/categorization'
require 'models/category'
require 'models/post'
require 'models/author'
require 'models/comment'
require 'models/tag'
require 'models/tagging'
require 'models/person'
require 'models/reader'
require 'models/parrot'
require 'models/pirate'
require 'models/treasure'
require 'models/price_estimate'
require 'models/club'
require 'models/member'
require 'models/membership'
require 'models/sponsor'
class ProjectWithAfterCreateHook < ActiveRecord::Base
set_table_name 'projects'
has_and_belongs_to_many :developers,
:class_name => "DeveloperForProjectWithAfterCreateHook",
:join_table => "developers_projects",
:foreign_key => "project_id",
:association_foreign_key => "developer_id"
after_create :add_david
def add_david
david = DeveloperForProjectWithAfterCreateHook.find_by_name('David')
david.projects << self
end
end
class DeveloperForProjectWithAfterCreateHook < ActiveRecord::Base
set_table_name 'developers'
has_and_belongs_to_many :projects,
:class_name => "ProjectWithAfterCreateHook",
:join_table => "developers_projects",
:association_foreign_key => "project_id",
:foreign_key => "developer_id"
end
class ProjectWithSymbolsForKeys < ActiveRecord::Base
set_table_name 'projects'
has_and_belongs_to_many :developers,
:class_name => "DeveloperWithSymbolsForKeys",
:join_table => :developers_projects,
:foreign_key => :project_id,
:association_foreign_key => "developer_id"
end
class DeveloperWithSymbolsForKeys < ActiveRecord::Base
set_table_name 'developers'
has_and_belongs_to_many :projects,
:class_name => "ProjectWithSymbolsForKeys",
:join_table => :developers_projects,
:association_foreign_key => :project_id,
:foreign_key => "developer_id"
end
class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
fixtures :accounts, :companies, :categories, :posts, :categories_posts, :developers, :projects, :developers_projects,
:parrots, :pirates, :treasures, :price_estimates
def test_has_and_belongs_to_many
david = Developer.find(1)
assert !david.projects.empty?
assert_equal 2, david.projects.size
active_record = Project.find(1)
assert !active_record.developers.empty?
assert_equal 3, active_record.developers.size
assert active_record.developers.include?(david)
end
def test_triple_equality
assert !(Array === Developer.find(1).projects)
assert Developer.find(1).projects === Array
end
def test_adding_single
jamis = Developer.find(2)
jamis.projects.reload # causing the collection to load
action_controller = Project.find(2)
assert_equal 1, jamis.projects.size
assert_equal 1, action_controller.developers.size
jamis.projects << action_controller
assert_equal 2, jamis.projects.size
assert_equal 2, jamis.projects(true).size
assert_equal 2, action_controller.developers(true).size
end
def test_adding_type_mismatch
jamis = Developer.find(2)
assert_raise(ActiveRecord::AssociationTypeMismatch) { jamis.projects << nil }
assert_raise(ActiveRecord::AssociationTypeMismatch) { jamis.projects << 1 }
end
def test_adding_from_the_project
jamis = Developer.find(2)
action_controller = Project.find(2)
action_controller.developers.reload
assert_equal 1, jamis.projects.size
assert_equal 1, action_controller.developers.size
action_controller.developers << jamis
assert_equal 2, jamis.projects(true).size
assert_equal 2, action_controller.developers.size
assert_equal 2, action_controller.developers(true).size
end
def test_adding_from_the_project_fixed_timestamp
jamis = Developer.find(2)
action_controller = Project.find(2)
action_controller.developers.reload
assert_equal 1, jamis.projects.size
assert_equal 1, action_controller.developers.size
updated_at = jamis.updated_at
action_controller.developers << jamis
assert_equal updated_at, jamis.updated_at
assert_equal 2, jamis.projects(true).size
assert_equal 2, action_controller.developers.size
assert_equal 2, action_controller.developers(true).size
end
def test_adding_multiple
aredridel = Developer.new("name" => "Aredridel")
aredridel.save
aredridel.projects.reload
aredridel.projects.push(Project.find(1), Project.find(2))
assert_equal 2, aredridel.projects.size
assert_equal 2, aredridel.projects(true).size
end
def test_adding_a_collection
aredridel = Developer.new("name" => "Aredridel")
aredridel.save
aredridel.projects.reload
aredridel.projects.concat([Project.find(1), Project.find(2)])
assert_equal 2, aredridel.projects.size
assert_equal 2, aredridel.projects(true).size
end
def test_adding_uses_default_values_on_join_table
ac = projects(:action_controller)
assert !developers(:jamis).projects.include?(ac)
developers(:jamis).projects << ac
assert developers(:jamis, :reload).projects.include?(ac)
project = developers(:jamis).projects.detect { |p| p == ac }
assert_equal 1, project.access_level.to_i
end
def test_habtm_attribute_access_and_respond_to
project = developers(:jamis).projects[0]
assert project.has_attribute?("name")
assert project.has_attribute?("joined_on")
assert project.has_attribute?("access_level")
assert project.respond_to?("name")
assert project.respond_to?("name=")
assert project.respond_to?("name?")
assert project.respond_to?("joined_on")
# given that the 'join attribute' won't be persisted, I don't
# think we should define the mutators
#assert project.respond_to?("joined_on=")
assert project.respond_to?("joined_on?")
assert project.respond_to?("access_level")
#assert project.respond_to?("access_level=")
assert project.respond_to?("access_level?")
end
def test_habtm_adding_before_save
no_of_devels = Developer.count
no_of_projects = Project.count
aredridel = Developer.new("name" => "Aredridel")
aredridel.projects.concat([Project.find(1), p = Project.new("name" => "Projekt")])
assert aredridel.new_record?
assert p.new_record?
assert aredridel.save
assert !aredridel.new_record?
assert_equal no_of_devels+1, Developer.count
assert_equal no_of_projects+1, Project.count
assert_equal 2, aredridel.projects.size
assert_equal 2, aredridel.projects(true).size
end
def test_habtm_saving_multiple_relationships
new_project = Project.new("name" => "Grimetime")
amount_of_developers = 4
developers = (0...amount_of_developers).collect {|i| Developer.create(:name => "JME #{i}") }.reverse
new_project.developer_ids = [developers[0].id, developers[1].id]
new_project.developers_with_callback_ids = [developers[2].id, developers[3].id]
assert new_project.save
new_project.reload
assert_equal amount_of_developers, new_project.developers.size
assert_equal developers, new_project.developers
end
def test_habtm_unique_order_preserved
assert_equal developers(:poor_jamis, :jamis, :david), projects(:active_record).non_unique_developers
assert_equal developers(:poor_jamis, :jamis, :david), projects(:active_record).developers
end
def test_build
devel = Developer.find(1)
proj = assert_no_queries { devel.projects.build("name" => "Projekt") }
assert !devel.projects.loaded?
assert_equal devel.projects.last, proj
assert devel.projects.loaded?
assert proj.new_record?
devel.save
assert !proj.new_record?
assert_equal devel.projects.last, proj
assert_equal Developer.find(1).projects.sort_by(&:id).last, proj # prove join table is updated
end
def test_build_by_new_record
devel = Developer.new(:name => "Marcel", :salary => 75000)
proj1 = devel.projects.build(:name => "Make bed")
proj2 = devel.projects.build(:name => "Lie in it")
assert_equal devel.projects.last, proj2
assert proj2.new_record?
devel.save
assert !devel.new_record?
assert !proj2.new_record?
assert_equal devel.projects.last, proj2
assert_equal Developer.find_by_name("Marcel").projects.last, proj2 # prove join table is updated
end
def test_create
devel = Developer.find(1)
proj = devel.projects.create("name" => "Projekt")
assert !devel.projects.loaded?
assert_equal devel.projects.last, proj
assert devel.projects.loaded?
assert !proj.new_record?
assert_equal Developer.find(1).projects.sort_by(&:id).last, proj # prove join table is updated
end
def test_create_by_new_record
devel = Developer.new(:name => "Marcel", :salary => 75000)
proj1 = devel.projects.build(:name => "Make bed")
proj2 = devel.projects.build(:name => "Lie in it")
assert_equal devel.projects.last, proj2
assert proj2.new_record?
devel.save
assert !devel.new_record?
assert !proj2.new_record?
assert_equal devel.projects.last, proj2
assert_equal Developer.find_by_name("Marcel").projects.last, proj2 # prove join table is updated
end
def test_creation_respects_hash_condition
post = categories(:general).post_with_conditions.build(:body => '')
assert post.save
assert_equal 'Yet Another Testing Title', post.title
another_post = categories(:general).post_with_conditions.create(:body => '')
assert !another_post.new_record?
assert_equal 'Yet Another Testing Title', another_post.title
end
def test_uniq_after_the_fact
dev = developers(:jamis)
dev.projects << projects(:active_record)
dev.projects << projects(:active_record)
assert_equal 3, dev.projects.size
assert_equal 1, dev.projects.uniq.size
end
def test_uniq_before_the_fact
projects(:active_record).developers << developers(:jamis)
projects(:active_record).developers << developers(:david)
assert_equal 3, projects(:active_record, :reload).developers.size
end
def test_deleting
david = Developer.find(1)
active_record = Project.find(1)
david.projects.reload
assert_equal 2, david.projects.size
assert_equal 3, active_record.developers.size
david.projects.delete(active_record)
assert_equal 1, david.projects.size
assert_equal 1, david.projects(true).size
assert_equal 2, active_record.developers(true).size
end
def test_deleting_array
david = Developer.find(1)
david.projects.reload
david.projects.delete(Project.find(:all))
assert_equal 0, david.projects.size
assert_equal 0, david.projects(true).size
end
def test_deleting_with_sql
david = Developer.find(1)
active_record = Project.find(1)
active_record.developers.reload
assert_equal 3, active_record.developers_by_sql.size
active_record.developers_by_sql.delete(david)
assert_equal 2, active_record.developers_by_sql(true).size
end
def test_deleting_array_with_sql
active_record = Project.find(1)
active_record.developers.reload
assert_equal 3, active_record.developers_by_sql.size
active_record.developers_by_sql.delete(Developer.find(:all))
assert_equal 0, active_record.developers_by_sql(true).size
end
def test_deleting_all
david = Developer.find(1)
david.projects.reload
david.projects.clear
assert_equal 0, david.projects.size
assert_equal 0, david.projects(true).size
end
def test_removing_associations_on_destroy
david = DeveloperWithBeforeDestroyRaise.find(1)
assert !david.projects.empty?
assert_nothing_raised { david.destroy }
assert david.projects.empty?
assert DeveloperWithBeforeDestroyRaise.connection.select_all("SELECT * FROM developers_projects WHERE developer_id = 1").empty?
end
def test_additional_columns_from_join_table
assert_date_from_db Date.new(2004, 10, 10), Developer.find(1).projects.first.joined_on.to_date
end
def test_destroy_all
david = Developer.find(1)
david.projects.reload
assert !david.projects.empty?
david.projects.destroy_all
assert david.projects.empty?
assert david.projects(true).empty?
end
def test_deprecated_push_with_attributes_was_removed
jamis = developers(:jamis)
assert_raise(NoMethodError) do
jamis.projects.push_with_attributes(projects(:action_controller), :joined_on => Date.today)
end
end
def test_associations_with_conditions
assert_equal 3, projects(:active_record).developers.size
assert_equal 1, projects(:active_record).developers_named_david.size
assert_equal 1, projects(:active_record).developers_named_david_with_hash_conditions.size
assert_equal developers(:david), projects(:active_record).developers_named_david.find(developers(:david).id)
assert_equal developers(:david), projects(:active_record).developers_named_david_with_hash_conditions.find(developers(:david).id)
assert_equal developers(:david), projects(:active_record).salaried_developers.find(developers(:david).id)
projects(:active_record).developers_named_david.clear
assert_equal 2, projects(:active_record, :reload).developers.size
end
def test_find_in_association
# Using sql
assert_equal developers(:david), projects(:active_record).developers.find(developers(:david).id), "SQL find"
# Using ruby
active_record = projects(:active_record)
active_record.developers.reload
assert_equal developers(:david), active_record.developers.find(developers(:david).id), "Ruby find"
end
def test_include_uses_array_include_after_loaded
project = projects(:active_record)
project.developers.class # force load target
developer = project.developers.first
assert_no_queries do
assert project.developers.loaded?
assert project.developers.include?(developer)
end
end
def test_include_checks_if_record_exists_if_target_not_loaded
project = projects(:active_record)
developer = project.developers.first
project.reload
assert ! project.developers.loaded?
assert_queries(1) do
assert project.developers.include?(developer)
end
assert ! project.developers.loaded?
end
def test_include_returns_false_for_non_matching_record_to_verify_scoping
project = projects(:active_record)
developer = Developer.create :name => "Bryan", :salary => 50_000
assert ! project.developers.loaded?
assert ! project.developers.include?(developer)
end
def test_find_in_association_with_custom_finder_sql
assert_equal developers(:david), projects(:active_record).developers_with_finder_sql.find(developers(:david).id), "SQL find"
active_record = projects(:active_record)
active_record.developers_with_finder_sql.reload
assert_equal developers(:david), active_record.developers_with_finder_sql.find(developers(:david).id), "Ruby find"
end
def test_find_in_association_with_custom_finder_sql_and_string_id
assert_equal developers(:david), projects(:active_record).developers_with_finder_sql.find(developers(:david).id.to_s), "SQL find"
end
def test_find_with_merged_options
assert_equal 1, projects(:active_record).limited_developers.size
assert_equal 1, projects(:active_record).limited_developers.find(:all).size
assert_equal 3, projects(:active_record).limited_developers.find(:all, :limit => nil).size
end
def test_dynamic_find_should_respect_association_order
# Developers are ordered 'name DESC, id DESC'
low_id_jamis = developers(:jamis)
middle_id_jamis = developers(:poor_jamis)
high_id_jamis = projects(:active_record).developers.create(:name => 'Jamis')
assert_equal high_id_jamis, projects(:active_record).developers.find(:first, :conditions => "name = 'Jamis'")
assert_equal high_id_jamis, projects(:active_record).developers.find_by_name('Jamis')
end
def test_dynamic_find_order_should_override_association_order
# Developers are ordered 'name DESC, id DESC'
low_id_jamis = developers(:jamis)
middle_id_jamis = developers(:poor_jamis)
high_id_jamis = projects(:active_record).developers.create(:name => 'Jamis')
assert_equal low_id_jamis, projects(:active_record).developers.find(:first, :conditions => "name = 'Jamis'", :order => 'id')
assert_equal low_id_jamis, projects(:active_record).developers.find_by_name('Jamis', :order => 'id')
end
def test_dynamic_find_all_should_respect_association_order
# Developers are ordered 'name DESC, id DESC'
low_id_jamis = developers(:jamis)
middle_id_jamis = developers(:poor_jamis)
high_id_jamis = projects(:active_record).developers.create(:name => 'Jamis')
assert_equal [high_id_jamis, middle_id_jamis, low_id_jamis], projects(:active_record).developers.find(:all, :conditions => "name = 'Jamis'")
assert_equal [high_id_jamis, middle_id_jamis, low_id_jamis], projects(:active_record).developers.find_all_by_name('Jamis')
end
def test_dynamic_find_all_order_should_override_association_order
# Developers are ordered 'name DESC, id DESC'
low_id_jamis = developers(:jamis)
middle_id_jamis = developers(:poor_jamis)
high_id_jamis = projects(:active_record).developers.create(:name => 'Jamis')
assert_equal [low_id_jamis, middle_id_jamis, high_id_jamis], projects(:active_record).developers.find(:all, :conditions => "name = 'Jamis'", :order => 'id')
assert_equal [low_id_jamis, middle_id_jamis, high_id_jamis], projects(:active_record).developers.find_all_by_name('Jamis', :order => 'id')
end
def test_dynamic_find_all_should_respect_association_limit
assert_equal 1, projects(:active_record).limited_developers.find(:all, :conditions => "name = 'Jamis'").length
assert_equal 1, projects(:active_record).limited_developers.find_all_by_name('Jamis').length
end
def test_dynamic_find_all_order_should_override_association_limit
assert_equal 2, projects(:active_record).limited_developers.find(:all, :conditions => "name = 'Jamis'", :limit => 9_000).length
assert_equal 2, projects(:active_record).limited_developers.find_all_by_name('Jamis', :limit => 9_000).length
end
def test_dynamic_find_all_should_respect_readonly_access
projects(:active_record).readonly_developers.each { |d| assert_raise(ActiveRecord::ReadOnlyRecord) { d.save! } if d.valid?}
projects(:active_record).readonly_developers.each { |d| d.readonly? }
end
def test_new_with_values_in_collection
jamis = DeveloperForProjectWithAfterCreateHook.find_by_name('Jamis')
david = DeveloperForProjectWithAfterCreateHook.find_by_name('David')
project = ProjectWithAfterCreateHook.new(:name => "Cooking with Bertie")
project.developers << jamis
project.save!
project.reload
assert project.developers.include?(jamis)
assert project.developers.include?(david)
end
def test_find_in_association_with_options
developers = projects(:active_record).developers.find(:all)
assert_equal 3, developers.size
assert_equal developers(:poor_jamis), projects(:active_record).developers.find(:first, :conditions => "salary < 10000")
assert_equal developers(:jamis), projects(:active_record).developers.find(:first, :order => "salary DESC")
end
def test_replace_with_less
david = developers(:david)
david.projects = [projects(:action_controller)]
assert david.save
assert_equal 1, david.projects.length
end
def test_replace_with_new
david = developers(:david)
david.projects = [projects(:action_controller), Project.new("name" => "ActionWebSearch")]
david.save
assert_equal 2, david.projects.length
assert !david.projects.include?(projects(:active_record))
end
def test_replace_on_new_object
new_developer = Developer.new("name" => "Matz")
new_developer.projects = [projects(:action_controller), Project.new("name" => "ActionWebSearch")]
new_developer.save
assert_equal 2, new_developer.projects.length
end
def test_consider_type
developer = Developer.find(:first)
special_project = SpecialProject.create("name" => "Special Project")
other_project = developer.projects.first
developer.special_projects << special_project
developer.reload
assert developer.projects.include?(special_project)
assert developer.special_projects.include?(special_project)
assert !developer.special_projects.include?(other_project)
end
def test_update_attributes_after_push_without_duplicate_join_table_rows
developer = Developer.new("name" => "Kano")
project = SpecialProject.create("name" => "Special Project")
assert developer.save
developer.projects << project
developer.update_attribute("name", "Bruza")
assert_equal 1, Developer.connection.select_value(<<-end_sql).to_i
SELECT count(*) FROM developers_projects
WHERE project_id = #{project.id}
AND developer_id = #{developer.id}
end_sql
end
def test_updating_attributes_on_non_rich_associations
welcome = categories(:technology).posts.first
welcome.title = "Something else"
assert welcome.save!
end
def test_habtm_respects_select
categories(:technology).select_testing_posts(true).each do |o|
assert_respond_to o, :correctness_marker
end
assert_respond_to categories(:technology).select_testing_posts.find(:first), :correctness_marker
end
def test_updating_attributes_on_rich_associations
david = projects(:action_controller).developers.first
david.name = "DHH"
assert_raises(ActiveRecord::ReadOnlyRecord) { david.save! }
end
def test_updating_attributes_on_rich_associations_with_limited_find_from_reflection
david = projects(:action_controller).selected_developers.first
david.name = "DHH"
assert_nothing_raised { david.save! }
end
def test_updating_attributes_on_rich_associations_with_limited_find
david = projects(:action_controller).developers.find(:all, :select => "developers.*").first
david.name = "DHH"
assert david.save!
end
def test_join_table_alias
assert_equal 3, Developer.find(:all, :include => {:projects => :developers}, :conditions => 'developers_projects_join.joined_on IS NOT NULL').size
end
def test_join_with_group
group = Developer.columns.inject([]) do |g, c|
g << "developers.#{c.name}"
g << "developers_projects_2.#{c.name}"
end
Project.columns.each { |c| group << "projects.#{c.name}" }
assert_equal 3, Developer.find(:all, :include => {:projects => :developers}, :conditions => 'developers_projects_join.joined_on IS NOT NULL', :group => group.join(",")).size
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
end
def test_assign_ids
developer = Developer.new("name" => "Joe")
developer.project_ids = projects(:active_record, :action_controller).map(&:id)
developer.save
developer.reload
assert_equal 2, developer.projects.length
assert_equal projects(:active_record), developer.projects[0]
assert_equal projects(:action_controller), developer.projects[1]
end
def test_assign_ids_ignoring_blanks
developer = Developer.new("name" => "Joe")
developer.project_ids = [projects(:active_record).id, nil, projects(:action_controller).id, '']
developer.save
developer.reload
assert_equal 2, developer.projects.length
assert_equal projects(:active_record), developer.projects[0]
assert_equal projects(:action_controller), developer.projects[1]
end
def test_select_limited_ids_list
# Set timestamps
Developer.transaction do
Developer.find(:all, :order => 'id').each_with_index do |record, i|
record.update_attributes(:created_at => 5.years.ago + (i * 5.minutes))
end
end
join_base = ActiveRecord::Associations::ClassMethods::JoinDependency::JoinBase.new(Project)
join_dep = ActiveRecord::Associations::ClassMethods::JoinDependency.new(join_base, :developers, nil)
projects = Project.send(:select_limited_ids_list, {:order => 'developers.created_at'}, join_dep)
assert !projects.include?("'"), projects
assert_equal %w(1 2), projects.scan(/\d/).sort
end
def test_scoped_find_on_through_association_doesnt_return_read_only_records
tag = Post.find(1).tags.find_by_name("General")
assert_nothing_raised do
tag.save!
end
end
def test_has_many_through_polymorphic_has_manys_works
assert_equal [10, 20].to_set, pirates(:redbeard).treasure_estimates.map(&:price).to_set
end
def test_symbols_as_keys
developer = DeveloperWithSymbolsForKeys.new(:name => 'David')
project = ProjectWithSymbolsForKeys.new(:name => 'Rails Testing')
project.developers << developer
project.save!
assert_equal 1, project.developers.size
assert_equal 1, developer.projects.size
assert_equal developer, project.developers.find(:first)
assert_equal project, developer.projects.find(:first)
end
end

View file

@ -1,932 +0,0 @@
require "cases/helper"
require 'models/developer'
require 'models/project'
require 'models/company'
require 'models/topic'
require 'models/reply'
require 'models/category'
require 'models/post'
require 'models/author'
require 'models/comment'
require 'models/person'
require 'models/reader'
class HasManyAssociationsTest < ActiveRecord::TestCase
fixtures :accounts, :categories, :companies, :developers, :projects,
:developers_projects, :topics, :authors, :comments, :author_addresses,
:people, :posts
def setup
Client.destroyed_client_ids.clear
end
def force_signal37_to_load_all_clients_of_firm
companies(:first_firm).clients_of_firm.each {|f| }
end
def test_counting_with_counter_sql
assert_equal 2, Firm.find(:first).clients.count
end
def test_counting
assert_equal 2, Firm.find(:first).plain_clients.count
end
def test_counting_with_empty_hash_conditions
assert_equal 2, Firm.find(:first).plain_clients.count(:conditions => {})
end
def test_counting_with_single_conditions
assert_equal 2, Firm.find(:first).plain_clients.count(:conditions => '1=1')
end
def test_counting_with_single_hash
assert_equal 2, Firm.find(:first).plain_clients.count(:conditions => '1=1')
end
def test_counting_with_column_name_and_hash
assert_equal 2, Firm.find(:first).plain_clients.count(:all, :conditions => '1=1')
end
def test_finding
assert_equal 2, Firm.find(:first).clients.length
end
def test_find_with_blank_conditions
[[], {}, nil, ""].each do |blank|
assert_equal 2, Firm.find(:first).clients.find(:all, :conditions => blank).size
end
end
def test_find_many_with_merged_options
assert_equal 1, companies(:first_firm).limited_clients.size
assert_equal 1, companies(:first_firm).limited_clients.find(:all).size
assert_equal 2, companies(:first_firm).limited_clients.find(:all, :limit => nil).size
end
def test_dynamic_find_should_respect_association_order
assert_equal companies(:second_client), companies(:first_firm).clients_sorted_desc.find(:first, :conditions => "type = 'Client'")
assert_equal companies(:second_client), companies(:first_firm).clients_sorted_desc.find_by_type('Client')
end
def test_dynamic_find_order_should_override_association_order
assert_equal companies(:first_client), companies(:first_firm).clients_sorted_desc.find(:first, :conditions => "type = 'Client'", :order => 'id')
assert_equal companies(:first_client), companies(:first_firm).clients_sorted_desc.find_by_type('Client', :order => 'id')
end
def test_dynamic_find_all_should_respect_association_order
assert_equal [companies(:second_client), companies(:first_client)], companies(:first_firm).clients_sorted_desc.find(:all, :conditions => "type = 'Client'")
assert_equal [companies(:second_client), companies(:first_client)], companies(:first_firm).clients_sorted_desc.find_all_by_type('Client')
end
def test_dynamic_find_all_order_should_override_association_order
assert_equal [companies(:first_client), companies(:second_client)], companies(:first_firm).clients_sorted_desc.find(:all, :conditions => "type = 'Client'", :order => 'id')
assert_equal [companies(:first_client), companies(:second_client)], companies(:first_firm).clients_sorted_desc.find_all_by_type('Client', :order => 'id')
end
def test_dynamic_find_all_should_respect_association_limit
assert_equal 1, companies(:first_firm).limited_clients.find(:all, :conditions => "type = 'Client'").length
assert_equal 1, companies(:first_firm).limited_clients.find_all_by_type('Client').length
end
def test_dynamic_find_all_limit_should_override_association_limit
assert_equal 2, companies(:first_firm).limited_clients.find(:all, :conditions => "type = 'Client'", :limit => 9_000).length
assert_equal 2, companies(:first_firm).limited_clients.find_all_by_type('Client', :limit => 9_000).length
end
def test_dynamic_find_all_should_respect_readonly_access
companies(:first_firm).readonly_clients.find(:all).each { |c| assert_raise(ActiveRecord::ReadOnlyRecord) { c.save! } }
companies(:first_firm).readonly_clients.find(:all).each { |c| assert c.readonly? }
end
def test_cant_save_has_many_readonly_association
authors(:david).readonly_comments.each { |c| assert_raise(ActiveRecord::ReadOnlyRecord) { c.save! } }
authors(:david).readonly_comments.each { |c| assert c.readonly? }
end
def test_triple_equality
assert !(Array === Firm.find(:first).clients)
assert Firm.find(:first).clients === Array
end
def test_finding_default_orders
assert_equal "Summit", Firm.find(:first).clients.first.name
end
def test_finding_with_different_class_name_and_order
assert_equal "Microsoft", Firm.find(:first).clients_sorted_desc.first.name
end
def test_finding_with_foreign_key
assert_equal "Microsoft", Firm.find(:first).clients_of_firm.first.name
end
def test_finding_with_condition
assert_equal "Microsoft", Firm.find(:first).clients_like_ms.first.name
end
def test_finding_with_condition_hash
assert_equal "Microsoft", Firm.find(:first).clients_like_ms_with_hash_conditions.first.name
end
def test_finding_using_sql
firm = Firm.find(:first)
first_client = firm.clients_using_sql.first
assert_not_nil first_client
assert_equal "Microsoft", first_client.name
assert_equal 1, firm.clients_using_sql.size
assert_equal 1, Firm.find(:first).clients_using_sql.size
end
def test_counting_using_sql
assert_equal 1, Firm.find(:first).clients_using_counter_sql.size
assert Firm.find(:first).clients_using_counter_sql.any?
assert_equal 0, Firm.find(:first).clients_using_zero_counter_sql.size
assert !Firm.find(:first).clients_using_zero_counter_sql.any?
end
def test_counting_non_existant_items_using_sql
assert_equal 0, Firm.find(:first).no_clients_using_counter_sql.size
end
def test_belongs_to_sanity
c = Client.new
assert_nil c.firm
if c.firm
assert false, "belongs_to failed if check"
end
unless c.firm
else
assert false, "belongs_to failed unless check"
end
end
def test_find_ids
firm = Firm.find(:first)
assert_raises(ActiveRecord::RecordNotFound) { firm.clients.find }
client = firm.clients.find(2)
assert_kind_of Client, client
client_ary = firm.clients.find([2])
assert_kind_of Array, client_ary
assert_equal client, client_ary.first
client_ary = firm.clients.find(2, 3)
assert_kind_of Array, client_ary
assert_equal 2, client_ary.size
assert_equal client, client_ary.first
assert_raises(ActiveRecord::RecordNotFound) { firm.clients.find(2, 99) }
end
def test_find_string_ids_when_using_finder_sql
firm = Firm.find(:first)
client = firm.clients_using_finder_sql.find("2")
assert_kind_of Client, client
client_ary = firm.clients_using_finder_sql.find(["2"])
assert_kind_of Array, client_ary
assert_equal client, client_ary.first
client_ary = firm.clients_using_finder_sql.find("2", "3")
assert_kind_of Array, client_ary
assert_equal 2, client_ary.size
assert client_ary.include?(client)
end
def test_find_all
firm = Firm.find(:first)
assert_equal 2, firm.clients.find(:all, :conditions => "#{QUOTED_TYPE} = 'Client'").length
assert_equal 1, firm.clients.find(:all, :conditions => "name = 'Summit'").length
end
def test_find_all_sanitized
firm = Firm.find(:first)
summit = firm.clients.find(:all, :conditions => "name = 'Summit'")
assert_equal summit, firm.clients.find(:all, :conditions => ["name = ?", "Summit"])
assert_equal summit, firm.clients.find(:all, :conditions => ["name = :name", { :name => "Summit" }])
end
def test_find_first
firm = Firm.find(:first)
client2 = Client.find(2)
assert_equal firm.clients.first, firm.clients.find(:first)
assert_equal client2, firm.clients.find(:first, :conditions => "#{QUOTED_TYPE} = 'Client'")
end
def test_find_first_sanitized
firm = Firm.find(:first)
client2 = Client.find(2)
assert_equal client2, firm.clients.find(:first, :conditions => ["#{QUOTED_TYPE} = ?", 'Client'])
assert_equal client2, firm.clients.find(:first, :conditions => ["#{QUOTED_TYPE} = :type", { :type => 'Client' }])
end
def test_find_in_collection
assert_equal Client.find(2).name, companies(:first_firm).clients.find(2).name
assert_raises(ActiveRecord::RecordNotFound) { companies(:first_firm).clients.find(6) }
end
def test_find_grouped
all_clients_of_firm1 = Client.find(:all, :conditions => "firm_id = 1")
grouped_clients_of_firm1 = Client.find(:all, :conditions => "firm_id = 1", :group => "firm_id", :select => 'firm_id, count(id) as clients_count')
assert_equal 2, all_clients_of_firm1.size
assert_equal 1, grouped_clients_of_firm1.size
end
def test_adding
force_signal37_to_load_all_clients_of_firm
natural = Client.new("name" => "Natural Company")
companies(:first_firm).clients_of_firm << natural
assert_equal 2, companies(:first_firm).clients_of_firm.size # checking via the collection
assert_equal 2, companies(:first_firm).clients_of_firm(true).size # checking using the db
assert_equal natural, companies(:first_firm).clients_of_firm.last
end
def test_adding_using_create
first_firm = companies(:first_firm)
assert_equal 2, first_firm.plain_clients.size
natural = first_firm.plain_clients.create(:name => "Natural Company")
assert_equal 3, first_firm.plain_clients.length
assert_equal 3, first_firm.plain_clients.size
end
def test_create_with_bang_on_has_many_when_parent_is_new_raises
assert_raises(ActiveRecord::RecordNotSaved) do
firm = Firm.new
firm.plain_clients.create! :name=>"Whoever"
end
end
def test_regular_create_on_has_many_when_parent_is_new_raises
assert_raises(ActiveRecord::RecordNotSaved) do
firm = Firm.new
firm.plain_clients.create :name=>"Whoever"
end
end
def test_create_with_bang_on_has_many_raises_when_record_not_saved
assert_raises(ActiveRecord::RecordInvalid) do
firm = Firm.find(:first)
firm.plain_clients.create!
end
end
def test_create_with_bang_on_habtm_when_parent_is_new_raises
assert_raises(ActiveRecord::RecordNotSaved) do
Developer.new("name" => "Aredridel").projects.create!
end
end
def test_adding_a_mismatch_class
assert_raises(ActiveRecord::AssociationTypeMismatch) { companies(:first_firm).clients_of_firm << nil }
assert_raises(ActiveRecord::AssociationTypeMismatch) { companies(:first_firm).clients_of_firm << 1 }
assert_raises(ActiveRecord::AssociationTypeMismatch) { companies(:first_firm).clients_of_firm << Topic.find(1) }
end
def test_adding_a_collection
force_signal37_to_load_all_clients_of_firm
companies(:first_firm).clients_of_firm.concat([Client.new("name" => "Natural Company"), Client.new("name" => "Apple")])
assert_equal 3, companies(:first_firm).clients_of_firm.size
assert_equal 3, companies(:first_firm).clients_of_firm(true).size
end
def test_adding_before_save
no_of_firms = Firm.count
no_of_clients = Client.count
new_firm = Firm.new("name" => "A New Firm, Inc")
c = Client.new("name" => "Apple")
new_firm.clients_of_firm.push Client.new("name" => "Natural Company")
assert_equal 1, new_firm.clients_of_firm.size
new_firm.clients_of_firm << c
assert_equal 2, new_firm.clients_of_firm.size
assert_equal no_of_firms, Firm.count # Firm was not saved to database.
assert_equal no_of_clients, Client.count # Clients were not saved to database.
assert new_firm.save
assert !new_firm.new_record?
assert !c.new_record?
assert_equal new_firm, c.firm
assert_equal no_of_firms+1, Firm.count # Firm was saved to database.
assert_equal no_of_clients+2, Client.count # Clients were saved to database.
assert_equal 2, new_firm.clients_of_firm.size
assert_equal 2, new_firm.clients_of_firm(true).size
end
def test_invalid_adding
firm = Firm.find(1)
assert !(firm.clients_of_firm << c = Client.new)
assert c.new_record?
assert !firm.valid?
assert !firm.save
assert c.new_record?
end
def test_invalid_adding_before_save
no_of_firms = Firm.count
no_of_clients = Client.count
new_firm = Firm.new("name" => "A New Firm, Inc")
new_firm.clients_of_firm.concat([c = Client.new, Client.new("name" => "Apple")])
assert c.new_record?
assert !c.valid?
assert !new_firm.valid?
assert !new_firm.save
assert c.new_record?
assert new_firm.new_record?
end
def test_build
company = companies(:first_firm)
new_client = assert_no_queries { company.clients_of_firm.build("name" => "Another Client") }
assert !company.clients_of_firm.loaded?
assert_equal "Another Client", new_client.name
assert new_client.new_record?
assert_equal new_client, company.clients_of_firm.last
company.name += '-changed'
assert_queries(2) { assert company.save }
assert !new_client.new_record?
assert_equal 2, company.clients_of_firm(true).size
end
def test_build_many
company = companies(:first_firm)
new_clients = assert_no_queries { company.clients_of_firm.build([{"name" => "Another Client"}, {"name" => "Another Client II"}]) }
assert_equal 2, new_clients.size
company.name += '-changed'
assert_queries(3) { assert company.save }
assert_equal 3, company.clients_of_firm(true).size
end
def test_build_followed_by_save_does_not_load_target
new_client = companies(:first_firm).clients_of_firm.build("name" => "Another Client")
assert companies(:first_firm).save
assert !companies(:first_firm).clients_of_firm.loaded?
end
def test_build_without_loading_association
first_topic = topics(:first)
Reply.column_names
assert_equal 1, first_topic.replies.length
assert_no_queries do
first_topic.replies.build(:title => "Not saved", :content => "Superstars")
assert_equal 2, first_topic.replies.size
end
assert_equal 2, first_topic.replies.to_ary.size
end
def test_create_without_loading_association
first_firm = companies(:first_firm)
Firm.column_names
Client.column_names
assert_equal 1, first_firm.clients_of_firm.size
first_firm.clients_of_firm.reset
assert_queries(1) do
first_firm.clients_of_firm.create(:name => "Superstars")
end
assert_equal 2, first_firm.clients_of_firm.size
end
def test_invalid_build
new_client = companies(:first_firm).clients_of_firm.build
assert new_client.new_record?
assert !new_client.valid?
assert_equal new_client, companies(:first_firm).clients_of_firm.last
assert !companies(:first_firm).save
assert new_client.new_record?
assert_equal 1, companies(:first_firm).clients_of_firm(true).size
end
def test_create
force_signal37_to_load_all_clients_of_firm
new_client = companies(:first_firm).clients_of_firm.create("name" => "Another Client")
assert !new_client.new_record?
assert_equal new_client, companies(:first_firm).clients_of_firm.last
assert_equal new_client, companies(:first_firm).clients_of_firm(true).last
end
def test_create_many
companies(:first_firm).clients_of_firm.create([{"name" => "Another Client"}, {"name" => "Another Client II"}])
assert_equal 3, companies(:first_firm).clients_of_firm(true).size
end
def test_create_followed_by_save_does_not_load_target
new_client = companies(:first_firm).clients_of_firm.create("name" => "Another Client")
assert companies(:first_firm).save
assert !companies(:first_firm).clients_of_firm.loaded?
end
def test_find_or_initialize
the_client = companies(:first_firm).clients.find_or_initialize_by_name("Yet another client")
assert_equal companies(:first_firm).id, the_client.firm_id
assert_equal "Yet another client", the_client.name
assert the_client.new_record?
end
def test_find_or_create
number_of_clients = companies(:first_firm).clients.size
the_client = companies(:first_firm).clients.find_or_create_by_name("Yet another client")
assert_equal number_of_clients + 1, companies(:first_firm, :reload).clients.size
assert_equal the_client, companies(:first_firm).clients.find_or_create_by_name("Yet another client")
assert_equal number_of_clients + 1, companies(:first_firm, :reload).clients.size
end
def test_deleting
force_signal37_to_load_all_clients_of_firm
companies(:first_firm).clients_of_firm.delete(companies(:first_firm).clients_of_firm.first)
assert_equal 0, companies(:first_firm).clients_of_firm.size
assert_equal 0, companies(:first_firm).clients_of_firm(true).size
end
def test_deleting_before_save
new_firm = Firm.new("name" => "A New Firm, Inc.")
new_client = new_firm.clients_of_firm.build("name" => "Another Client")
assert_equal 1, new_firm.clients_of_firm.size
new_firm.clients_of_firm.delete(new_client)
assert_equal 0, new_firm.clients_of_firm.size
end
def test_deleting_a_collection
force_signal37_to_load_all_clients_of_firm
companies(:first_firm).clients_of_firm.create("name" => "Another Client")
assert_equal 2, companies(:first_firm).clients_of_firm.size
companies(:first_firm).clients_of_firm.delete([companies(:first_firm).clients_of_firm[0], companies(:first_firm).clients_of_firm[1]])
assert_equal 0, companies(:first_firm).clients_of_firm.size
assert_equal 0, companies(:first_firm).clients_of_firm(true).size
end
def test_delete_all
force_signal37_to_load_all_clients_of_firm
companies(:first_firm).clients_of_firm.create("name" => "Another Client")
assert_equal 2, companies(:first_firm).clients_of_firm.size
companies(:first_firm).clients_of_firm.delete_all
assert_equal 0, companies(:first_firm).clients_of_firm.size
assert_equal 0, companies(:first_firm).clients_of_firm(true).size
end
def test_delete_all_with_not_yet_loaded_association_collection
force_signal37_to_load_all_clients_of_firm
companies(:first_firm).clients_of_firm.create("name" => "Another Client")
assert_equal 2, companies(:first_firm).clients_of_firm.size
companies(:first_firm).clients_of_firm.reset
companies(:first_firm).clients_of_firm.delete_all
assert_equal 0, companies(:first_firm).clients_of_firm.size
assert_equal 0, companies(:first_firm).clients_of_firm(true).size
end
def test_clearing_an_association_collection
firm = companies(:first_firm)
client_id = firm.clients_of_firm.first.id
assert_equal 1, firm.clients_of_firm.size
firm.clients_of_firm.clear
assert_equal 0, firm.clients_of_firm.size
assert_equal 0, firm.clients_of_firm(true).size
assert_equal [], Client.destroyed_client_ids[firm.id]
# Should not be destroyed since the association is not dependent.
assert_nothing_raised do
assert Client.find(client_id).firm.nil?
end
end
def test_clearing_a_dependent_association_collection
firm = companies(:first_firm)
client_id = firm.dependent_clients_of_firm.first.id
assert_equal 1, firm.dependent_clients_of_firm.size
# :dependent means destroy is called on each client
firm.dependent_clients_of_firm.clear
assert_equal 0, firm.dependent_clients_of_firm.size
assert_equal 0, firm.dependent_clients_of_firm(true).size
assert_equal [client_id], Client.destroyed_client_ids[firm.id]
# Should be destroyed since the association is dependent.
assert Client.find_by_id(client_id).nil?
end
def test_clearing_an_exclusively_dependent_association_collection
firm = companies(:first_firm)
client_id = firm.exclusively_dependent_clients_of_firm.first.id
assert_equal 1, firm.exclusively_dependent_clients_of_firm.size
assert_equal [], Client.destroyed_client_ids[firm.id]
# :exclusively_dependent means each client is deleted directly from
# the database without looping through them calling destroy.
firm.exclusively_dependent_clients_of_firm.clear
assert_equal 0, firm.exclusively_dependent_clients_of_firm.size
assert_equal 0, firm.exclusively_dependent_clients_of_firm(true).size
# no destroy-filters should have been called
assert_equal [], Client.destroyed_client_ids[firm.id]
# Should be destroyed since the association is exclusively dependent.
assert Client.find_by_id(client_id).nil?
end
def test_dependent_association_respects_optional_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_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_dependent_association_respects_optional_sanitized_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
assert ms_client.save
assert_equal 'Microsoft', ms_client.name
another_ms_client = companies(:first_firm).clients_like_ms_with_hash_conditions.create
assert !another_ms_client.new_record?
assert_equal 'Microsoft', another_ms_client.name
end
def test_dependent_delete_and_destroy_with_belongs_to
author_address = author_addresses(:david_address)
assert_equal [], AuthorAddress.destroyed_author_address_ids[authors(:david).id]
assert_difference "AuthorAddress.count", -2 do
authors(:david).destroy
end
assert_equal [author_address.id], AuthorAddress.destroyed_author_address_ids[authors(:david).id]
end
def test_invalid_belongs_to_dependent_option_raises_exception
assert_raises ArgumentError do
Author.belongs_to :special_author_address, :dependent => :nullify
end
end
def test_clearing_without_initial_access
firm = companies(:first_firm)
firm.clients_of_firm.clear
assert_equal 0, firm.clients_of_firm.size
assert_equal 0, firm.clients_of_firm(true).size
end
def test_deleting_a_item_which_is_not_in_the_collection
force_signal37_to_load_all_clients_of_firm
summit = Client.find_by_name('Summit')
companies(:first_firm).clients_of_firm.delete(summit)
assert_equal 1, companies(:first_firm).clients_of_firm.size
assert_equal 1, companies(:first_firm).clients_of_firm(true).size
assert_equal 2, summit.client_of
end
def test_deleting_type_mismatch
david = Developer.find(1)
david.projects.reload
assert_raises(ActiveRecord::AssociationTypeMismatch) { david.projects.delete(1) }
end
def test_deleting_self_type_mismatch
david = Developer.find(1)
david.projects.reload
assert_raises(ActiveRecord::AssociationTypeMismatch) { david.projects.delete(Project.find(1).developers) }
end
def test_destroy_all
force_signal37_to_load_all_clients_of_firm
assert !companies(:first_firm).clients_of_firm.empty?, "37signals has clients after load"
companies(:first_firm).clients_of_firm.destroy_all
assert companies(:first_firm).clients_of_firm.empty?, "37signals has no clients after destroy all"
assert companies(:first_firm).clients_of_firm(true).empty?, "37signals has no clients after destroy all and refresh"
end
def test_dependence
firm = companies(:first_firm)
assert_equal 2, firm.clients.size
firm.destroy
assert Client.find(:all, :conditions => "firm_id=#{firm.id}").empty?
end
def test_destroy_dependent_when_deleted_from_association
firm = Firm.find(:first)
assert_equal 2, firm.clients.size
client = firm.clients.first
firm.clients.delete(client)
assert_raise(ActiveRecord::RecordNotFound) { Client.find(client.id) }
assert_raise(ActiveRecord::RecordNotFound) { firm.clients.find(client.id) }
assert_equal 1, firm.clients.size
end
def test_three_levels_of_dependence
topic = Topic.create "title" => "neat and simple"
reply = topic.replies.create "title" => "neat and simple", "content" => "still digging it"
silly_reply = reply.replies.create "title" => "neat and simple", "content" => "ain't complaining"
assert_nothing_raised { topic.destroy }
end
uses_transaction :test_dependence_with_transaction_support_on_failure
def test_dependence_with_transaction_support_on_failure
firm = companies(:first_firm)
clients = firm.clients
assert_equal 2, clients.length
clients.last.instance_eval { def before_destroy() raise "Trigger rollback" end }
firm.destroy rescue "do nothing"
assert_equal 2, Client.find(:all, :conditions => "firm_id=#{firm.id}").size
end
def test_dependence_on_account
num_accounts = Account.count
companies(:first_firm).destroy
assert_equal num_accounts - 1, Account.count
end
def test_depends_and_nullify
num_accounts = Account.count
num_companies = Company.count
core = companies(:rails_core)
assert_equal accounts(:rails_core_account), core.account
assert_equal companies(:leetsoft, :jadedpixel), core.companies
core.destroy
assert_nil accounts(:rails_core_account).reload.firm_id
assert_nil companies(:leetsoft).reload.client_of
assert_nil companies(:jadedpixel).reload.client_of
assert_equal num_accounts, Account.count
end
def test_included_in_collection
assert companies(:first_firm).clients.include?(Client.find(2))
end
def test_adding_array_and_collection
assert_nothing_raised { Firm.find(:first).clients + Firm.find(:all).last.clients }
end
def test_find_all_without_conditions
firm = companies(:first_firm)
assert_equal 2, firm.clients.find(:all).length
end
def test_replace_with_less
firm = Firm.find(:first)
firm.clients = [companies(:first_client)]
assert firm.save, "Could not save firm"
firm.reload
assert_equal 1, firm.clients.length
end
def test_replace_with_less_and_dependent_nullify
num_companies = Company.count
companies(:rails_core).companies = []
assert_equal num_companies, Company.count
end
def test_replace_with_new
firm = Firm.find(:first)
firm.clients = [companies(:second_client), Client.new("name" => "New Client")]
firm.save
firm.reload
assert_equal 2, firm.clients.length
assert !firm.clients.include?(:first_client)
end
def test_replace_on_new_object
firm = Firm.new("name" => "New Firm")
firm.clients = [companies(:second_client), Client.new("name" => "New Client")]
assert firm.save
firm.reload
assert_equal 2, firm.clients.length
assert firm.clients.include?(Client.find_by_name("New Client"))
end
def test_get_ids
assert_equal [companies(:first_client).id, companies(:second_client).id], companies(:first_firm).client_ids
end
def test_assign_ids
firm = Firm.new("name" => "Apple")
firm.client_ids = [companies(:first_client).id, companies(:second_client).id]
firm.save
firm.reload
assert_equal 2, firm.clients.length
assert firm.clients.include?(companies(:second_client))
end
def test_assign_ids_ignoring_blanks
firm = Firm.create!(:name => 'Apple')
firm.client_ids = [companies(:first_client).id, nil, companies(:second_client).id, '']
firm.save!
assert_equal 2, firm.clients(true).size
assert firm.clients.include?(companies(:second_client))
end
def test_get_ids_for_through
assert_equal [comments(:eager_other_comment1).id], authors(:mary).comment_ids
end
def test_modifying_a_through_a_has_many_should_raise
[
lambda { authors(:mary).comment_ids = [comments(:greetings).id, comments(:more_greetings).id] },
lambda { authors(:mary).comments = [comments(:greetings), comments(:more_greetings)] },
lambda { authors(:mary).comments << Comment.create!(:body => "Yay", :post_id => 424242) },
lambda { authors(:mary).comments.delete(authors(:mary).comments.first) },
].each {|block| assert_raise(ActiveRecord::HasManyThroughCantAssociateThroughHasManyReflection, &block) }
end
def test_assign_ids_for_through_a_belongs_to
post = Post.new(:title => "Assigning IDs works!", :body => "You heared it here first, folks!")
post.person_ids = [people(:david).id, people(:michael).id]
post.save
post.reload
assert_equal 2, post.people.length
assert post.people.include?(people(:david))
end
def test_dynamic_find_should_respect_association_order_for_through
assert_equal Comment.find(10), authors(:david).comments_desc.find(:first, :conditions => "comments.type = 'SpecialComment'")
assert_equal Comment.find(10), authors(:david).comments_desc.find_by_type('SpecialComment')
end
def test_dynamic_find_order_should_override_association_order_for_through
assert_equal Comment.find(3), authors(:david).comments_desc.find(:first, :conditions => "comments.type = 'SpecialComment'", :order => 'comments.id')
assert_equal Comment.find(3), authors(:david).comments_desc.find_by_type('SpecialComment', :order => 'comments.id')
end
def test_dynamic_find_all_should_respect_association_order_for_through
assert_equal [Comment.find(10), Comment.find(7), Comment.find(6), Comment.find(3)], authors(:david).comments_desc.find(:all, :conditions => "comments.type = 'SpecialComment'")
assert_equal [Comment.find(10), Comment.find(7), Comment.find(6), Comment.find(3)], authors(:david).comments_desc.find_all_by_type('SpecialComment')
end
def test_dynamic_find_all_order_should_override_association_order_for_through
assert_equal [Comment.find(3), Comment.find(6), Comment.find(7), Comment.find(10)], authors(:david).comments_desc.find(:all, :conditions => "comments.type = 'SpecialComment'", :order => 'comments.id')
assert_equal [Comment.find(3), Comment.find(6), Comment.find(7), Comment.find(10)], authors(:david).comments_desc.find_all_by_type('SpecialComment', :order => 'comments.id')
end
def test_dynamic_find_all_should_respect_association_limit_for_through
assert_equal 1, authors(:david).limited_comments.find(:all, :conditions => "comments.type = 'SpecialComment'").length
assert_equal 1, authors(:david).limited_comments.find_all_by_type('SpecialComment').length
end
def test_dynamic_find_all_order_should_override_association_limit_for_through
assert_equal 4, authors(:david).limited_comments.find(:all, :conditions => "comments.type = 'SpecialComment'", :limit => 9_000).length
assert_equal 4, authors(:david).limited_comments.find_all_by_type('SpecialComment', :limit => 9_000).length
end
def test_find_all_include_over_the_same_table_for_through
assert_equal 2, people(:michael).posts.find(:all, :include => :people).length
end
def test_has_many_through_respects_hash_conditions
assert_equal authors(:david).hello_posts, authors(:david).hello_posts_with_hash_conditions
assert_equal authors(:david).hello_post_comments, authors(:david).hello_post_comments_with_hash_conditions
end
def test_include_uses_array_include_after_loaded
firm = companies(:first_firm)
firm.clients.class # force load target
client = firm.clients.first
assert_no_queries do
assert firm.clients.loaded?
assert firm.clients.include?(client)
end
end
def test_include_checks_if_record_exists_if_target_not_loaded
firm = companies(:first_firm)
client = firm.clients.first
firm.reload
assert ! firm.clients.loaded?
assert_queries(1) do
assert firm.clients.include?(client)
end
assert ! firm.clients.loaded?
end
def test_include_loads_collection_if_target_uses_finder_sql
firm = companies(:first_firm)
client = firm.clients_using_sql.first
firm.reload
assert ! firm.clients_using_sql.loaded?
assert firm.clients_using_sql.include?(client)
assert firm.clients_using_sql.loaded?
end
def test_include_returns_false_for_non_matching_record_to_verify_scoping
firm = companies(:first_firm)
client = Client.create!(:name => 'Not Associated')
assert ! firm.clients.loaded?
assert ! firm.clients.include?(client)
end
def test_calling_first_or_last_on_association_should_not_load_association
firm = companies(:first_firm)
firm.clients.first
firm.clients.last
assert !firm.clients.loaded?
end
def test_calling_first_or_last_on_loaded_association_should_not_fetch_with_query
firm = companies(:first_firm)
firm.clients.class # force load target
assert firm.clients.loaded?
assert_no_queries do
firm.clients.first
assert_equal 2, firm.clients.first(2).size
firm.clients.last
assert_equal 2, firm.clients.last(2).size
end
end
def test_calling_first_or_last_on_existing_record_with_build_should_load_association
firm = companies(:first_firm)
firm.clients.build(:name => 'Foo')
assert !firm.clients.loaded?
assert_queries 1 do
firm.clients.first
firm.clients.last
end
assert firm.clients.loaded?
end
def test_calling_first_or_last_on_new_record_should_not_run_queries
firm = Firm.new
assert_no_queries do
firm.clients.first
firm.clients.last
end
end
def test_calling_first_or_last_with_find_options_on_loaded_association_should_fetch_with_query
firm = companies(:first_firm)
firm.clients.class # force load target
assert_queries 2 do
assert firm.clients.loaded?
firm.clients.first(:order => 'name')
firm.clients.last(:order => 'name')
end
end
def test_calling_first_or_last_with_integer_on_association_should_load_association
firm = companies(:first_firm)
assert_queries 1 do
firm.clients.first(2)
firm.clients.last(2)
end
assert firm.clients.loaded?
end
end

View file

@ -1,190 +0,0 @@
require "cases/helper"
require 'models/post'
require 'models/person'
require 'models/reader'
class HasManyThroughAssociationsTest < ActiveRecord::TestCase
fixtures :posts, :readers, :people
def test_associate_existing
assert_queries(2) { posts(:thinking);people(:david) }
assert_queries(1) do
posts(:thinking).people << people(:david)
end
assert_queries(1) do
assert posts(:thinking).people.include?(people(:david))
end
assert posts(:thinking).reload.people(true).include?(people(:david))
end
def test_associating_new
assert_queries(1) { posts(:thinking) }
new_person = nil # so block binding catches it
assert_queries(0) do
new_person = Person.new :first_name => 'bob'
end
# Associating new records always saves them
# Thus, 1 query for the new person record, 1 query for the new join table record
assert_queries(2) do
posts(:thinking).people << new_person
end
assert_queries(1) do
assert posts(:thinking).people.include?(new_person)
end
assert posts(:thinking).reload.people(true).include?(new_person)
end
def test_associate_new_by_building
assert_queries(1) { posts(:thinking) }
assert_queries(0) do
posts(:thinking).people.build(:first_name=>"Bob")
posts(:thinking).people.new(:first_name=>"Ted")
end
# Should only need to load the association once
assert_queries(1) do
assert posts(:thinking).people.collect(&:first_name).include?("Bob")
assert posts(:thinking).people.collect(&:first_name).include?("Ted")
end
# 2 queries for each new record (1 to save the record itself, 1 for the join model)
# * 2 new records = 4
# + 1 query to save the actual post = 5
assert_queries(5) do
posts(:thinking).body += '-changed'
posts(:thinking).save
end
assert posts(:thinking).reload.people(true).collect(&:first_name).include?("Bob")
assert posts(:thinking).reload.people(true).collect(&:first_name).include?("Ted")
end
def test_delete_association
assert_queries(2){posts(:welcome);people(:michael); }
assert_queries(1) do
posts(:welcome).people.delete(people(:michael))
end
assert_queries(1) do
assert posts(:welcome).people.empty?
end
assert posts(:welcome).reload.people(true).empty?
end
def test_replace_association
assert_queries(4){posts(:welcome);people(:david);people(:michael); posts(:welcome).people(true)}
# 1 query to delete the existing reader (michael)
# 1 query to associate the new reader (david)
assert_queries(2) do
posts(:welcome).people = [people(:david)]
end
assert_queries(0){
assert posts(:welcome).people.include?(people(:david))
assert !posts(:welcome).people.include?(people(:michael))
}
assert posts(:welcome).reload.people(true).include?(people(:david))
assert !posts(:welcome).reload.people(true).include?(people(:michael))
end
def test_associate_with_create
assert_queries(1) { posts(:thinking) }
# 1 query for the new record, 1 for the join table record
# No need to update the actual collection yet!
assert_queries(2) do
posts(:thinking).people.create(:first_name=>"Jeb")
end
# *Now* we actually need the collection so it's loaded
assert_queries(1) do
assert posts(:thinking).people.collect(&:first_name).include?("Jeb")
end
assert posts(:thinking).reload.people(true).collect(&:first_name).include?("Jeb")
end
def test_associate_with_create_and_no_options
peeps = posts(:thinking).people.count
posts(:thinking).people.create(:first_name => 'foo')
assert_equal peeps + 1, posts(:thinking).people.count
end
def test_associate_with_create_exclamation_and_no_options
peeps = posts(:thinking).people.count
posts(:thinking).people.create!(:first_name => 'foo')
assert_equal peeps + 1, posts(:thinking).people.count
end
def test_clear_associations
assert_queries(2) { posts(:welcome);posts(:welcome).people(true) }
assert_queries(1) do
posts(:welcome).people.clear
end
assert_queries(0) do
assert posts(:welcome).people.empty?
end
assert posts(:welcome).reload.people(true).empty?
end
def test_association_callback_ordering
Post.reset_log
log = Post.log
post = posts(:thinking)
post.people_with_callbacks << people(:michael)
assert_equal [
[:added, :before, "Michael"],
[:added, :after, "Michael"]
], log.last(2)
post.people_with_callbacks.push(people(:david), Person.create!(:first_name => "Bob"), Person.new(:first_name => "Lary"))
assert_equal [
[:added, :before, "David"],
[:added, :after, "David"],
[:added, :before, "Bob"],
[:added, :after, "Bob"],
[:added, :before, "Lary"],
[:added, :after, "Lary"]
],log.last(6)
post.people_with_callbacks.build(:first_name => "Ted")
assert_equal [
[:added, :before, "Ted"],
[:added, :after, "Ted"]
], log.last(2)
post.people_with_callbacks.create(:first_name => "Sam")
assert_equal [
[:added, :before, "Sam"],
[:added, :after, "Sam"]
], log.last(2)
post.people_with_callbacks = [people(:michael),people(:david), Person.new(:first_name => "Julian"), Person.create!(:first_name => "Roger")]
assert_equal (%w(Ted Bob Sam Lary) * 2).sort, log[-12..-5].collect(&:last).sort
assert_equal [
[:added, :before, "Julian"],
[:added, :after, "Julian"],
[:added, :before, "Roger"],
[:added, :after, "Roger"]
], log.last(4)
post.people_with_callbacks.clear
assert_equal (%w(Michael David Julian Roger) * 2).sort, log.last(8).collect(&:last).sort
end
end

View file

@ -1,323 +0,0 @@
require "cases/helper"
require 'models/developer'
require 'models/project'
require 'models/company'
class HasOneAssociationsTest < ActiveRecord::TestCase
fixtures :accounts, :companies, :developers, :projects, :developers_projects
def setup
Account.destroyed_account_ids.clear
end
def test_has_one
assert_equal companies(:first_firm).account, Account.find(1)
assert_equal Account.find(1).credit_limit, companies(:first_firm).account.credit_limit
end
def test_has_one_cache_nils
firm = companies(:another_firm)
assert_queries(1) { assert_nil firm.account }
assert_queries(0) { assert_nil firm.account }
firms = Firm.find(:all, :include => :account)
assert_queries(0) { firms.each(&:account) }
end
def test_with_select
assert_equal Firm.find(1).account_with_select.attributes.size, 2
assert_equal Firm.find(1, :include => :account_with_select).account_with_select.attributes.size, 2
end
def test_can_marshal_has_one_association_with_nil_target
firm = Firm.new
assert_nothing_raised do
assert_equal firm.attributes, Marshal.load(Marshal.dump(firm)).attributes
end
firm.account
assert_nothing_raised do
assert_equal firm.attributes, Marshal.load(Marshal.dump(firm)).attributes
end
end
def test_proxy_assignment
company = companies(:first_firm)
assert_nothing_raised { company.account = company.account }
end
def test_triple_equality
assert Account === companies(:first_firm).account
assert companies(:first_firm).account === Account
end
def test_type_mismatch
assert_raises(ActiveRecord::AssociationTypeMismatch) { companies(:first_firm).account = 1 }
assert_raises(ActiveRecord::AssociationTypeMismatch) { companies(:first_firm).account = Project.find(1) }
end
def test_natural_assignment
apple = Firm.create("name" => "Apple")
citibank = Account.create("credit_limit" => 10)
apple.account = citibank
assert_equal apple.id, citibank.firm_id
end
def test_natural_assignment_to_nil
old_account_id = companies(:first_firm).account.id
companies(:first_firm).account = nil
companies(:first_firm).save
assert_nil companies(:first_firm).account
# account is dependent, therefore is destroyed when reference to owner is lost
assert_raises(ActiveRecord::RecordNotFound) { Account.find(old_account_id) }
end
def test_assignment_without_replacement
apple = Firm.create("name" => "Apple")
citibank = Account.create("credit_limit" => 10)
apple.account = citibank
assert_equal apple.id, citibank.firm_id
hsbc = apple.build_account({ :credit_limit => 20}, false)
assert_equal apple.id, hsbc.firm_id
hsbc.save
assert_equal apple.id, citibank.firm_id
nykredit = apple.create_account({ :credit_limit => 30}, false)
assert_equal apple.id, nykredit.firm_id
assert_equal apple.id, citibank.firm_id
assert_equal apple.id, hsbc.firm_id
end
def test_assignment_without_replacement_on_create
apple = Firm.create("name" => "Apple")
citibank = Account.create("credit_limit" => 10)
apple.account = citibank
assert_equal apple.id, citibank.firm_id
hsbc = apple.create_account({:credit_limit => 10}, false)
assert_equal apple.id, hsbc.firm_id
hsbc.save
assert_equal apple.id, citibank.firm_id
end
def test_dependence
num_accounts = Account.count
firm = Firm.find(1)
assert !firm.account.nil?
account_id = firm.account.id
assert_equal [], Account.destroyed_account_ids[firm.id]
firm.destroy
assert_equal num_accounts - 1, Account.count
assert_equal [account_id], Account.destroyed_account_ids[firm.id]
end
def test_exclusive_dependence
num_accounts = Account.count
firm = ExclusivelyDependentFirm.find(9)
assert !firm.account.nil?
account_id = firm.account.id
assert_equal [], Account.destroyed_account_ids[firm.id]
firm.destroy
assert_equal num_accounts - 1, Account.count
assert_equal [], Account.destroyed_account_ids[firm.id]
end
def test_dependence_with_nil_associate
firm = DependentFirm.new(:name => 'nullify')
firm.save!
assert_nothing_raised { firm.destroy }
end
def test_succesful_build_association
firm = Firm.new("name" => "GlobalMegaCorp")
firm.save
account = firm.build_account("credit_limit" => 1000)
assert account.save
assert_equal account, firm.account
end
def test_failing_build_association
firm = Firm.new("name" => "GlobalMegaCorp")
firm.save
account = firm.build_account
assert !account.save
assert_equal "can't be empty", account.errors.on("credit_limit")
end
def test_build_association_twice_without_saving_affects_nothing
count_of_account = Account.count
firm = Firm.find(:first)
account1 = firm.build_account("credit_limit" => 1000)
account2 = firm.build_account("credit_limit" => 2000)
assert_equal count_of_account, Account.count
end
def test_create_association
firm = Firm.create(:name => "GlobalMegaCorp")
account = firm.create_account(:credit_limit => 1000)
assert_equal account, firm.reload.account
end
def test_build
firm = Firm.new("name" => "GlobalMegaCorp")
firm.save
firm.account = account = Account.new("credit_limit" => 1000)
assert_equal account, firm.account
assert account.save
assert_equal account, firm.account
end
def test_build_before_child_saved
firm = Firm.find(1)
account = firm.account.build("credit_limit" => 1000)
assert_equal account, firm.account
assert account.new_record?
assert firm.save
assert_equal account, firm.account
assert !account.new_record?
end
def test_build_before_either_saved
firm = Firm.new("name" => "GlobalMegaCorp")
firm.account = account = Account.new("credit_limit" => 1000)
assert_equal account, firm.account
assert account.new_record?
assert firm.save
assert_equal account, firm.account
assert !account.new_record?
end
def test_failing_build_association
firm = Firm.new("name" => "GlobalMegaCorp")
firm.save
firm.account = account = Account.new
assert_equal account, firm.account
assert !account.save
assert_equal account, firm.account
assert_equal "can't be empty", account.errors.on("credit_limit")
end
def test_create
firm = Firm.new("name" => "GlobalMegaCorp")
firm.save
firm.account = account = Account.create("credit_limit" => 1000)
assert_equal account, firm.account
end
def test_create_before_save
firm = Firm.new("name" => "GlobalMegaCorp")
firm.account = account = Account.create("credit_limit" => 1000)
assert_equal account, firm.account
end
def test_dependence_with_missing_association
Account.destroy_all
firm = Firm.find(1)
assert firm.account.nil?
firm.destroy
end
def test_dependence_with_missing_association_and_nullify
Account.destroy_all
firm = DependentFirm.find(:first)
assert firm.account.nil?
firm.destroy
end
def test_assignment_before_parent_saved
firm = Firm.new("name" => "GlobalMegaCorp")
firm.account = a = Account.find(1)
assert firm.new_record?
assert_equal a, firm.account
assert firm.save
assert_equal a, firm.account
assert_equal a, firm.account(true)
end
def test_finding_with_interpolated_condition
firm = Firm.find(:first)
superior = firm.clients.create(:name => 'SuperiorCo')
superior.rating = 10
superior.save
assert_equal 10, firm.clients_with_interpolated_conditions.first.rating
end
def test_assignment_before_child_saved
firm = Firm.find(1)
firm.account = a = Account.new("credit_limit" => 1000)
assert !a.new_record?
assert_equal a, firm.account
assert_equal a, firm.account
assert_equal a, firm.account(true)
end
def test_save_fails_for_invalid_has_one
firm = Firm.find(:first)
assert firm.valid?
firm.account = Account.new
assert !firm.account.valid?
assert !firm.valid?
assert !firm.save
assert_equal "is invalid", firm.errors.on("account")
end
def test_assignment_before_either_saved
firm = Firm.new("name" => "GlobalMegaCorp")
firm.account = a = Account.new("credit_limit" => 1000)
assert firm.new_record?
assert a.new_record?
assert_equal a, firm.account
assert firm.save
assert !firm.new_record?
assert !a.new_record?
assert_equal a, firm.account
assert_equal a, firm.account(true)
end
def test_not_resaved_when_unchanged
firm = Firm.find(:first, :include => :account)
firm.name += '-changed'
assert_queries(1) { firm.save! }
firm = Firm.find(:first)
firm.account = Account.find(:first)
assert_queries(Firm.partial_updates? ? 0 : 1) { firm.save! }
firm = Firm.find(:first).clone
firm.account = Account.find(:first)
assert_queries(2) { firm.save! }
firm = Firm.find(:first).clone
firm.account = Account.find(:first).clone
assert_queries(2) { firm.save! }
end
def test_save_still_works_after_accessing_nil_has_one
jp = Company.new :name => 'Jaded Pixel'
jp.dummy_account.nil?
assert_nothing_raised do
jp.save!
end
end
def test_cant_save_readonly_association
assert_raise(ActiveRecord::ReadOnlyRecord) { companies(:first_firm).readonly_account.save! }
assert companies(:first_firm).readonly_account.readonly?
end
end

View file

@ -1,74 +0,0 @@
require "cases/helper"
require 'models/club'
require 'models/member'
require 'models/membership'
require 'models/sponsor'
class HasOneThroughAssociationsTest < ActiveRecord::TestCase
fixtures :members, :clubs, :memberships, :sponsors
def setup
@member = members(:groucho)
end
def test_has_one_through_with_has_one
assert_equal clubs(:boring_club), @member.club
end
def test_has_one_through_with_has_many
assert_equal clubs(:moustache_club), @member.favourite_club
end
def test_creating_association_creates_through_record
new_member = Member.create(:name => "Chris")
new_member.club = Club.create(:name => "LRUG")
assert_not_nil new_member.current_membership
assert_not_nil new_member.club
end
def test_replace_target_record
new_club = Club.create(:name => "Marx Bros")
@member.club = new_club
@member.reload
assert_equal new_club, @member.club
end
def test_replacing_target_record_deletes_old_association
assert_no_difference "Membership.count" do
new_club = Club.create(:name => "Bananarama")
@member.club = new_club
@member.reload
end
end
def test_has_one_through_polymorphic
assert_equal clubs(:moustache_club), @member.sponsor_club
end
def has_one_through_to_has_many
assert_equal 2, @member.fellow_members.size
end
def test_has_one_through_eager_loading
members = Member.find(:all, :include => :club, :conditions => ["name = ?", "Groucho Marx"])
assert_equal 1, members.size
assert_not_nil assert_no_queries {members[0].club}
end
def test_has_one_through_eager_loading_through_polymorphic
members = Member.find(:all, :include => :sponsor_club, :conditions => ["name = ?", "Groucho Marx"])
assert_equal 1, members.size
assert_not_nil assert_no_queries {members[0].sponsor_club}
end
def test_has_one_through_polymorphic_with_source_type
assert_equal members(:groucho), clubs(:moustache_club).sponsored_member
end
def test_eager_has_one_through_polymorphic_with_source_type
clubs = Club.find(:all, :include => :sponsored_member, :conditions => ["name = ?","Moustache and Eyebrow Fancier Club"])
# Only the eyebrow fanciers club has a sponsored_member
assert_not_nil assert_no_queries {clubs[0].sponsored_member}
end
end

View file

@ -1,88 +0,0 @@
require "cases/helper"
require 'models/post'
require 'models/comment'
require 'models/author'
require 'models/category'
require 'models/categorization'
class InnerJoinAssociationTest < ActiveRecord::TestCase
fixtures :authors, :posts, :comments, :categories, :categories_posts, :categorizations
def test_construct_finder_sql_creates_inner_joins
sql = Author.send(:construct_finder_sql, :joins => :posts)
assert_match /INNER JOIN .?posts.? ON .?posts.?.author_id = authors.id/, sql
end
def test_construct_finder_sql_cascades_inner_joins
sql = Author.send(:construct_finder_sql, :joins => {:posts => :comments})
assert_match /INNER JOIN .?posts.? ON .?posts.?.author_id = authors.id/, sql
assert_match /INNER JOIN .?comments.? ON .?comments.?.post_id = posts.id/, sql
end
def test_construct_finder_sql_inner_joins_through_associations
sql = Author.send(:construct_finder_sql, :joins => :categorized_posts)
assert_match /INNER JOIN .?categorizations.?.*INNER JOIN .?posts.?/, sql
end
def test_construct_finder_sql_applies_association_conditions
sql = Author.send(:construct_finder_sql, :joins => :categories_like_general, :conditions => "TERMINATING_MARKER")
assert_match /INNER JOIN .?categories.? ON.*AND.*.?General.?.*TERMINATING_MARKER/, sql
end
def test_construct_finder_sql_unpacks_nested_joins
sql = Author.send(:construct_finder_sql, :joins => {:posts => [[:comments]]})
assert_no_match /inner join.*inner join.*inner join/i, sql, "only two join clauses should be present"
assert_match /INNER JOIN .?posts.? ON .?posts.?.author_id = authors.id/, sql
assert_match /INNER JOIN .?comments.? ON .?comments.?.post_id = .?posts.?.id/, sql
end
def test_construct_finder_sql_ignores_empty_joins_hash
sql = Author.send(:construct_finder_sql, :joins => {})
assert_no_match /JOIN/i, sql
end
def test_construct_finder_sql_ignores_empty_joins_array
sql = Author.send(:construct_finder_sql, :joins => [])
assert_no_match /JOIN/i, sql
end
def test_find_with_implicit_inner_joins_honors_readonly_without_select
authors = Author.find(:all, :joins => :posts)
assert !authors.empty?, "expected authors to be non-empty"
assert authors.all? {|a| a.readonly? }, "expected all authors to be readonly"
end
def test_find_with_implicit_inner_joins_honors_readonly_with_select
authors = Author.find(:all, :select => 'authors.*', :joins => :posts)
assert !authors.empty?, "expected authors to be non-empty"
assert authors.all? {|a| !a.readonly? }, "expected no authors to be readonly"
end
def test_find_with_implicit_inner_joins_honors_readonly_false
authors = Author.find(:all, :joins => :posts, :readonly => false)
assert !authors.empty?, "expected authors to be non-empty"
assert authors.all? {|a| !a.readonly? }, "expected no authors to be readonly"
end
def test_find_with_implicit_inner_joins_does_not_set_associations
authors = Author.find(:all, :select => 'authors.*', :joins => :posts)
assert !authors.empty?, "expected authors to be non-empty"
assert authors.all? {|a| !a.send(:instance_variable_names).include?("@posts")}, "expected no authors to have the @posts association loaded"
end
def test_count_honors_implicit_inner_joins
real_count = Author.find(:all).sum{|a| a.posts.count }
assert_equal real_count, Author.count(:joins => :posts), "plain inner join count should match the number of referenced posts records"
end
def test_calculate_honors_implicit_inner_joins
real_count = Author.find(:all).sum{|a| a.posts.count }
assert_equal real_count, Author.calculate(:count, 'authors.id', :joins => :posts), "plain inner join count should match the number of referenced posts records"
end
def test_calculate_honors_implicit_inner_joins_and_distinct_and_conditions
real_count = Author.find(:all).select {|a| a.posts.any? {|p| p.title =~ /^Welcome/} }.length
authors_with_welcoming_post_titles = Author.calculate(:count, 'authors.id', :joins => :posts, :distinct => true, :conditions => "posts.title like 'Welcome%'")
assert_equal real_count, authors_with_welcoming_post_titles, "inner join and conditions should have only returned authors posting titles starting with 'Welcome'"
end
end

View file

@ -1,707 +0,0 @@
require "cases/helper"
require 'models/tag'
require 'models/tagging'
require 'models/post'
require 'models/item'
require 'models/comment'
require 'models/author'
require 'models/category'
require 'models/categorization'
require 'models/vertex'
require 'models/edge'
require 'models/book'
require 'models/citation'
class AssociationsJoinModelTest < ActiveRecord::TestCase
self.use_transactional_fixtures = false
fixtures :posts, :authors, :categories, :categorizations, :comments, :tags, :taggings, :author_favorites, :vertices, :items, :books
def test_has_many
assert authors(:david).categories.include?(categories(:general))
end
def test_has_many_inherited
assert authors(:mary).categories.include?(categories(:sti_test))
end
def test_inherited_has_many
assert categories(:sti_test).authors.include?(authors(:mary))
end
def test_has_many_uniq_through_join_model
assert_equal 2, authors(:mary).categorized_posts.size
assert_equal 1, authors(:mary).unique_categorized_posts.size
end
def test_has_many_uniq_through_count
author = authors(:mary)
assert !authors(:mary).unique_categorized_posts.loaded?
assert_queries(1) { assert_equal 1, author.unique_categorized_posts.count }
assert_queries(1) { assert_equal 1, author.unique_categorized_posts.count(:title) }
assert_queries(1) { assert_equal 0, author.unique_categorized_posts.count(:title, :conditions => "title is NULL") }
assert !authors(:mary).unique_categorized_posts.loaded?
end
def test_has_many_uniq_through_find
assert_equal 1, authors(:mary).unique_categorized_posts.find(:all).size
end
def test_has_many_uniq_through_dynamic_find
assert_equal 1, authors(:mary).unique_categorized_posts.find_all_by_title("So I was thinking").size
end
def test_polymorphic_has_many
assert posts(:welcome).taggings.include?(taggings(:welcome_general))
end
def test_polymorphic_has_one
assert_equal taggings(:welcome_general), posts(:welcome).tagging
end
def test_polymorphic_belongs_to
assert_equal posts(:welcome), posts(:welcome).taggings.first.taggable
end
def test_polymorphic_has_many_going_through_join_model
assert_equal tags(:general), tag = posts(:welcome).tags.first
assert_no_queries do
tag.tagging
end
end
def test_count_polymorphic_has_many
assert_equal 1, posts(:welcome).taggings.count
assert_equal 1, posts(:welcome).tags.count
end
def test_polymorphic_has_many_going_through_join_model_with_find
assert_equal tags(:general), tag = posts(:welcome).tags.find(:first)
assert_no_queries do
tag.tagging
end
end
def test_polymorphic_has_many_going_through_join_model_with_include_on_source_reflection
assert_equal tags(:general), tag = posts(:welcome).funky_tags.first
assert_no_queries do
tag.tagging
end
end
def test_polymorphic_has_many_going_through_join_model_with_include_on_source_reflection_with_find
assert_equal tags(:general), tag = posts(:welcome).funky_tags.find(:first)
assert_no_queries do
tag.tagging
end
end
def test_polymorphic_has_many_going_through_join_model_with_disabled_include
assert_equal tags(:general), tag = posts(:welcome).tags.add_joins_and_select.first
assert_queries 1 do
tag.tagging
end
end
def test_polymorphic_has_many_going_through_join_model_with_custom_select_and_joins
assert_equal tags(:general), tag = posts(:welcome).tags.add_joins_and_select.first
tag.author_id
end
def test_polymorphic_has_many_going_through_join_model_with_custom_foreign_key
assert_equal tags(:misc), taggings(:welcome_general).super_tag
assert_equal tags(:misc), posts(:welcome).super_tags.first
end
def test_polymorphic_has_many_create_model_with_inheritance_and_custom_base_class
post = SubStiPost.create :title => 'SubStiPost', :body => 'SubStiPost body'
assert_instance_of SubStiPost, post
tagging = tags(:misc).taggings.create(:taggable => post)
assert_equal "SubStiPost", tagging.taggable_type
end
def test_polymorphic_has_many_going_through_join_model_with_inheritance
assert_equal tags(:general), posts(:thinking).tags.first
end
def test_polymorphic_has_many_going_through_join_model_with_inheritance_with_custom_class_name
assert_equal tags(:general), posts(:thinking).funky_tags.first
end
def test_polymorphic_has_many_create_model_with_inheritance
post = posts(:thinking)
assert_instance_of SpecialPost, post
tagging = tags(:misc).taggings.create(:taggable => post)
assert_equal "Post", tagging.taggable_type
end
def test_polymorphic_has_one_create_model_with_inheritance
tagging = tags(:misc).create_tagging(:taggable => posts(:thinking))
assert_equal "Post", tagging.taggable_type
end
def test_set_polymorphic_has_many
tagging = tags(:misc).taggings.create
posts(:thinking).taggings << tagging
assert_equal "Post", tagging.taggable_type
end
def test_set_polymorphic_has_one
tagging = tags(:misc).taggings.create
posts(:thinking).tagging = tagging
assert_equal "Post", tagging.taggable_type
end
def test_create_polymorphic_has_many_with_scope
old_count = posts(:welcome).taggings.count
tagging = posts(:welcome).taggings.create(:tag => tags(:misc))
assert_equal "Post", tagging.taggable_type
assert_equal old_count+1, posts(:welcome).taggings.count
end
def test_create_bang_polymorphic_with_has_many_scope
old_count = posts(:welcome).taggings.count
tagging = posts(:welcome).taggings.create!(:tag => tags(:misc))
assert_equal "Post", tagging.taggable_type
assert_equal old_count+1, posts(:welcome).taggings.count
end
def test_create_polymorphic_has_one_with_scope
old_count = Tagging.count
tagging = posts(:welcome).tagging.create(:tag => tags(:misc))
assert_equal "Post", tagging.taggable_type
assert_equal old_count+1, Tagging.count
end
def test_delete_polymorphic_has_many_with_delete_all
assert_equal 1, posts(:welcome).taggings.count
posts(:welcome).taggings.first.update_attribute :taggable_type, 'PostWithHasManyDeleteAll'
post = find_post_with_dependency(1, :has_many, :taggings, :delete_all)
old_count = Tagging.count
post.destroy
assert_equal old_count-1, Tagging.count
assert_equal 0, posts(:welcome).taggings.count
end
def test_delete_polymorphic_has_many_with_destroy
assert_equal 1, posts(:welcome).taggings.count
posts(:welcome).taggings.first.update_attribute :taggable_type, 'PostWithHasManyDestroy'
post = find_post_with_dependency(1, :has_many, :taggings, :destroy)
old_count = Tagging.count
post.destroy
assert_equal old_count-1, Tagging.count
assert_equal 0, posts(:welcome).taggings.count
end
def test_delete_polymorphic_has_many_with_nullify
assert_equal 1, posts(:welcome).taggings.count
posts(:welcome).taggings.first.update_attribute :taggable_type, 'PostWithHasManyNullify'
post = find_post_with_dependency(1, :has_many, :taggings, :nullify)
old_count = Tagging.count
post.destroy
assert_equal old_count, Tagging.count
assert_equal 0, posts(:welcome).taggings.count
end
def test_delete_polymorphic_has_one_with_destroy
assert posts(:welcome).tagging
posts(:welcome).tagging.update_attribute :taggable_type, 'PostWithHasOneDestroy'
post = find_post_with_dependency(1, :has_one, :tagging, :destroy)
old_count = Tagging.count
post.destroy
assert_equal old_count-1, Tagging.count
assert_nil posts(:welcome).tagging(true)
end
def test_delete_polymorphic_has_one_with_nullify
assert posts(:welcome).tagging
posts(:welcome).tagging.update_attribute :taggable_type, 'PostWithHasOneNullify'
post = find_post_with_dependency(1, :has_one, :tagging, :nullify)
old_count = Tagging.count
post.destroy
assert_equal old_count, Tagging.count
assert_nil posts(:welcome).tagging(true)
end
def test_has_many_with_piggyback
assert_equal "2", categories(:sti_test).authors.first.post_id.to_s
end
def test_include_has_many_through
posts = Post.find(:all, :order => 'posts.id')
posts_with_authors = Post.find(:all, :include => :authors, :order => 'posts.id')
assert_equal posts.length, posts_with_authors.length
posts.length.times do |i|
assert_equal posts[i].authors.length, assert_no_queries { posts_with_authors[i].authors.length }
end
end
def test_include_polymorphic_has_one
post = Post.find_by_id(posts(:welcome).id, :include => :tagging)
tagging = taggings(:welcome_general)
assert_no_queries do
assert_equal tagging, post.tagging
end
end
def test_include_polymorphic_has_one_defined_in_abstract_parent
item = Item.find_by_id(items(:dvd).id, :include => :tagging)
tagging = taggings(:godfather)
assert_no_queries do
assert_equal tagging, item.tagging
end
end
def test_include_polymorphic_has_many_through
posts = Post.find(:all, :order => 'posts.id')
posts_with_tags = Post.find(:all, :include => :tags, :order => 'posts.id')
assert_equal posts.length, posts_with_tags.length
posts.length.times do |i|
assert_equal posts[i].tags.length, assert_no_queries { posts_with_tags[i].tags.length }
end
end
def test_include_polymorphic_has_many
posts = Post.find(:all, :order => 'posts.id')
posts_with_taggings = Post.find(:all, :include => :taggings, :order => 'posts.id')
assert_equal posts.length, posts_with_taggings.length
posts.length.times do |i|
assert_equal posts[i].taggings.length, assert_no_queries { posts_with_taggings[i].taggings.length }
end
end
def test_has_many_find_all
assert_equal [categories(:general)], authors(:david).categories.find(:all)
end
def test_has_many_find_first
assert_equal categories(:general), authors(:david).categories.find(:first)
end
def test_has_many_with_hash_conditions
assert_equal categories(:general), authors(:david).categories_like_general.find(:first)
end
def test_has_many_find_conditions
assert_equal categories(:general), authors(:david).categories.find(:first, :conditions => "categories.name = 'General'")
assert_equal nil, authors(:david).categories.find(:first, :conditions => "categories.name = 'Technology'")
end
def test_has_many_class_methods_called_by_method_missing
assert_equal categories(:general), authors(:david).categories.find_all_by_name('General').first
assert_equal nil, authors(:david).categories.find_by_name('Technology')
end
def test_has_many_array_methods_called_by_method_missing
assert true, authors(:david).categories.any? { |category| category.name == 'General' }
assert_nothing_raised { authors(:david).categories.sort }
end
def test_has_many_going_through_join_model_with_custom_foreign_key
assert_equal [], posts(:thinking).authors
assert_equal [authors(:mary)], posts(:authorless).authors
end
def test_both_scoped_and_explicit_joins_should_be_respected
assert_nothing_raised do
Post.send(:with_scope, :find => {:joins => "left outer join comments on comments.id = posts.id"}) do
Post.find :all, :select => "comments.id, authors.id", :joins => "left outer join authors on authors.id = posts.author_id"
end
end
end
def test_belongs_to_polymorphic_with_counter_cache
assert_equal 0, posts(:welcome)[:taggings_count]
tagging = posts(:welcome).taggings.create(:tag => tags(:general))
assert_equal 1, posts(:welcome, :reload)[:taggings_count]
tagging.destroy
assert posts(:welcome, :reload)[:taggings_count].zero?
end
def test_unavailable_through_reflection
assert_raise(ActiveRecord::HasManyThroughAssociationNotFoundError) { authors(:david).nothings }
end
def test_has_many_through_join_model_with_conditions
assert_equal [], posts(:welcome).invalid_taggings
assert_equal [], posts(:welcome).invalid_tags
end
def test_has_many_polymorphic
assert_raise ActiveRecord::HasManyThroughAssociationPolymorphicError do
assert_equal posts(:welcome, :thinking), tags(:general).taggables
end
assert_raise ActiveRecord::EagerLoadPolymorphicError do
assert_equal posts(:welcome, :thinking), tags(:general).taggings.find(:all, :include => :taggable, :conditions => 'bogus_table.column = 1')
end
end
def test_has_many_polymorphic_with_source_type
assert_equal posts(:welcome, :thinking), tags(:general).tagged_posts
end
def test_eager_has_many_polymorphic_with_source_type
tag_with_include = Tag.find(tags(:general).id, :include => :tagged_posts)
desired = posts(:welcome, :thinking)
assert_no_queries do
assert_equal desired, tag_with_include.tagged_posts
end
assert_equal 5, tag_with_include.taggings.length
end
def test_has_many_through_has_many_find_all
assert_equal comments(:greetings), authors(:david).comments.find(:all, :order => 'comments.id').first
end
def test_has_many_through_has_many_find_all_with_custom_class
assert_equal comments(:greetings), authors(:david).funky_comments.find(:all, :order => 'comments.id').first
end
def test_has_many_through_has_many_find_first
assert_equal comments(:greetings), authors(:david).comments.find(:first, :order => 'comments.id')
end
def test_has_many_through_has_many_find_conditions
options = { :conditions => "comments.#{QUOTED_TYPE}='SpecialComment'", :order => 'comments.id' }
assert_equal comments(:does_it_hurt), authors(:david).comments.find(:first, options)
end
def test_has_many_through_has_many_find_by_id
assert_equal comments(:more_greetings), authors(:david).comments.find(2)
end
def test_has_many_through_polymorphic_has_one
assert_raise(ActiveRecord::HasManyThroughSourceAssociationMacroError) { authors(:david).tagging }
end
def test_has_many_through_polymorphic_has_many
assert_equal taggings(:welcome_general, :thinking_general), authors(:david).taggings.uniq.sort_by { |t| t.id }
end
def test_include_has_many_through_polymorphic_has_many
author = Author.find_by_id(authors(:david).id, :include => :taggings)
expected_taggings = taggings(:welcome_general, :thinking_general)
assert_no_queries do
assert_equal expected_taggings, author.taggings.uniq.sort_by { |t| t.id }
end
end
def test_has_many_through_has_many_through
assert_raise(ActiveRecord::HasManyThroughSourceAssociationMacroError) { authors(:david).tags }
end
def test_has_many_through_habtm
assert_raise(ActiveRecord::HasManyThroughSourceAssociationMacroError) { authors(:david).post_categories }
end
def test_eager_load_has_many_through_has_many
author = Author.find :first, :conditions => ['name = ?', 'David'], :include => :comments, :order => 'comments.id'
SpecialComment.new; VerySpecialComment.new
assert_no_queries do
assert_equal [1,2,3,5,6,7,8,9,10], author.comments.collect(&:id)
end
end
def test_eager_load_has_many_through_has_many_with_conditions
post = Post.find(:first, :include => :invalid_tags)
assert_no_queries do
post.invalid_tags
end
end
def test_eager_belongs_to_and_has_one_not_singularized
assert_nothing_raised do
Author.find(:first, :include => :author_address)
AuthorAddress.find(:first, :include => :author)
end
end
def test_self_referential_has_many_through
assert_equal [authors(:mary)], authors(:david).favorite_authors
assert_equal [], authors(:mary).favorite_authors
end
def test_add_to_self_referential_has_many_through
new_author = Author.create(:name => "Bob")
authors(:david).author_favorites.create :favorite_author => new_author
assert_equal new_author, authors(:david).reload.favorite_authors.first
end
def test_has_many_through_uses_conditions_specified_on_the_has_many_association
author = Author.find(:first)
assert !author.comments.blank?
assert author.nonexistant_comments.blank?
end
def test_has_many_through_uses_correct_attributes
assert_nil posts(:thinking).tags.find_by_name("General").attributes["tag_id"]
end
def test_associating_unsaved_records_with_has_many_through
saved_post = posts(:thinking)
new_tag = Tag.new(:name => "new")
saved_post.tags << new_tag
assert !new_tag.new_record? #consistent with habtm!
assert !saved_post.new_record?
assert saved_post.tags.include?(new_tag)
assert !new_tag.new_record?
assert saved_post.reload.tags(true).include?(new_tag)
new_post = Post.new(:title => "Association replacmenet works!", :body => "You best believe it.")
saved_tag = tags(:general)
new_post.tags << saved_tag
assert new_post.new_record?
assert !saved_tag.new_record?
assert new_post.tags.include?(saved_tag)
new_post.save!
assert !new_post.new_record?
assert new_post.reload.tags(true).include?(saved_tag)
assert posts(:thinking).tags.build.new_record?
assert posts(:thinking).tags.new.new_record?
end
def test_create_associate_when_adding_to_has_many_through
count = posts(:thinking).tags.count
push = Tag.create!(:name => 'pushme')
post_thinking = posts(:thinking)
assert_nothing_raised { post_thinking.tags << push }
assert_nil( wrong = post_thinking.tags.detect { |t| t.class != Tag },
message = "Expected a Tag in tags collection, got #{wrong.class}.")
assert_nil( wrong = post_thinking.taggings.detect { |t| t.class != Tagging },
message = "Expected a Tagging in taggings collection, got #{wrong.class}.")
assert_equal(count + 1, post_thinking.tags.size)
assert_equal(count + 1, post_thinking.tags(true).size)
assert_kind_of Tag, post_thinking.tags.create!(:name => 'foo')
assert_nil( wrong = post_thinking.tags.detect { |t| t.class != Tag },
message = "Expected a Tag in tags collection, got #{wrong.class}.")
assert_nil( wrong = post_thinking.taggings.detect { |t| t.class != Tagging },
message = "Expected a Tagging in taggings collection, got #{wrong.class}.")
assert_equal(count + 2, post_thinking.tags.size)
assert_equal(count + 2, post_thinking.tags(true).size)
assert_nothing_raised { post_thinking.tags.concat(Tag.create!(:name => 'abc'), Tag.create!(:name => 'def')) }
assert_nil( wrong = post_thinking.tags.detect { |t| t.class != Tag },
message = "Expected a Tag in tags collection, got #{wrong.class}.")
assert_nil( wrong = post_thinking.taggings.detect { |t| t.class != Tagging },
message = "Expected a Tagging in taggings collection, got #{wrong.class}.")
assert_equal(count + 4, post_thinking.tags.size)
assert_equal(count + 4, post_thinking.tags(true).size)
# Raises if the wrong reflection name is used to set the Edge belongs_to
assert_nothing_raised { vertices(:vertex_1).sinks << vertices(:vertex_5) }
end
def test_has_many_through_collection_size_doesnt_load_target_if_not_loaded
author = authors(:david)
assert_equal 9, author.comments.size
assert !author.comments.loaded?
end
uses_mocha('has_many_through_collection_size_uses_counter_cache_if_it_exists') do
def test_has_many_through_collection_size_uses_counter_cache_if_it_exists
author = authors(:david)
author.stubs(:read_attribute).with('comments_count').returns(100)
assert_equal 100, author.comments.size
assert !author.comments.loaded?
end
end
def test_adding_junk_to_has_many_through_should_raise_type_mismatch
assert_raise(ActiveRecord::AssociationTypeMismatch) { posts(:thinking).tags << "Uhh what now?" }
end
def test_adding_to_has_many_through_should_return_self
tags = posts(:thinking).tags
assert_equal tags, posts(:thinking).tags.push(tags(:general))
end
def test_delete_associate_when_deleting_from_has_many_through_with_nonstandard_id
count = books(:awdr).references.count
references_before = books(:awdr).references
book = Book.create!(:name => 'Getting Real')
book_awdr = books(:awdr)
book_awdr.references << book
assert_equal(count + 1, book_awdr.references(true).size)
assert_nothing_raised { book_awdr.references.delete(book) }
assert_equal(count, book_awdr.references.size)
assert_equal(count, book_awdr.references(true).size)
assert_equal(references_before.sort, book_awdr.references.sort)
end
def test_delete_associate_when_deleting_from_has_many_through
count = posts(:thinking).tags.count
tags_before = posts(:thinking).tags
tag = Tag.create!(:name => 'doomed')
post_thinking = posts(:thinking)
post_thinking.tags << tag
assert_equal(count + 1, post_thinking.taggings(true).size)
assert_equal(count + 1, post_thinking.tags(true).size)
assert_nothing_raised { post_thinking.tags.delete(tag) }
assert_equal(count, post_thinking.tags.size)
assert_equal(count, post_thinking.tags(true).size)
assert_equal(count, post_thinking.taggings(true).size)
assert_equal(tags_before.sort, post_thinking.tags.sort)
end
def test_delete_associate_when_deleting_from_has_many_through_with_multiple_tags
count = posts(:thinking).tags.count
tags_before = posts(:thinking).tags
doomed = Tag.create!(:name => 'doomed')
doomed2 = Tag.create!(:name => 'doomed2')
quaked = Tag.create!(:name => 'quaked')
post_thinking = posts(:thinking)
post_thinking.tags << doomed << doomed2
assert_equal(count + 2, post_thinking.tags(true).size)
assert_nothing_raised { post_thinking.tags.delete(doomed, doomed2, quaked) }
assert_equal(count, post_thinking.tags.size)
assert_equal(count, post_thinking.tags(true).size)
assert_equal(tags_before.sort, post_thinking.tags.sort)
end
def test_deleting_junk_from_has_many_through_should_raise_type_mismatch
assert_raise(ActiveRecord::AssociationTypeMismatch) { posts(:thinking).tags.delete("Uhh what now?") }
end
def test_has_many_through_sum_uses_calculations
assert_nothing_raised { authors(:david).comments.sum(:post_id) }
end
def test_calculations_on_has_many_through_should_disambiguate_fields
assert_nothing_raised { authors(:david).categories.maximum(:id) }
end
def test_calculations_on_has_many_through_should_not_disambiguate_fields_unless_necessary
assert_nothing_raised { authors(:david).categories.maximum("categories.id") }
end
def test_has_many_through_has_many_with_sti
assert_equal [comments(:does_it_hurt)], authors(:david).special_post_comments
end
def test_uniq_has_many_through_should_retain_order
comment_ids = authors(:david).comments.map(&:id)
assert_equal comment_ids.sort, authors(:david).ordered_uniq_comments.map(&:id)
assert_equal comment_ids.sort.reverse, authors(:david).ordered_uniq_comments_desc.map(&:id)
end
def test_polymorphic_has_many
expected = taggings(:welcome_general)
p = Post.find(posts(:welcome).id, :include => :taggings)
assert_no_queries {assert p.taggings.include?(expected)}
assert posts(:welcome).taggings.include?(taggings(:welcome_general))
end
def test_polymorphic_has_one
expected = posts(:welcome)
tagging = Tagging.find(taggings(:welcome_general).id, :include => :taggable)
assert_no_queries { assert_equal expected, tagging.taggable}
end
def test_polymorphic_belongs_to
p = Post.find(posts(:welcome).id, :include => {:taggings => :taggable})
assert_no_queries {assert_equal posts(:welcome), p.taggings.first.taggable}
end
def test_preload_polymorphic_has_many_through
posts = Post.find(:all, :order => 'posts.id')
posts_with_tags = Post.find(:all, :include => :tags, :order => 'posts.id')
assert_equal posts.length, posts_with_tags.length
posts.length.times do |i|
assert_equal posts[i].tags.length, assert_no_queries { posts_with_tags[i].tags.length }
end
end
def test_preload_polymorph_many_types
taggings = Tagging.find :all, :include => :taggable, :conditions => ['taggable_type != ?', 'FakeModel']
assert_no_queries do
taggings.first.taggable.id
taggings[1].taggable.id
end
taggables = taggings.map(&:taggable)
assert taggables.include?(items(:dvd))
assert taggables.include?(posts(:welcome))
end
def test_preload_nil_polymorphic_belongs_to
assert_nothing_raised do
taggings = Tagging.find(:all, :include => :taggable, :conditions => ['taggable_type IS NULL'])
end
end
def test_preload_polymorphic_has_many
posts = Post.find(:all, :order => 'posts.id')
posts_with_taggings = Post.find(:all, :include => :taggings, :order => 'posts.id')
assert_equal posts.length, posts_with_taggings.length
posts.length.times do |i|
assert_equal posts[i].taggings.length, assert_no_queries { posts_with_taggings[i].taggings.length }
end
end
def test_belongs_to_shared_parent
comments = Comment.find(:all, :include => :post, :conditions => 'post_id = 1')
assert_no_queries do
assert_equal comments.first.post, comments[1].post
end
end
def test_has_many_through_include_uses_array_include_after_loaded
david = authors(:david)
david.categories.class # force load target
category = david.categories.first
assert_no_queries do
assert david.categories.loaded?
assert david.categories.include?(category)
end
end
def test_has_many_through_include_checks_if_record_exists_if_target_not_loaded
david = authors(:david)
category = david.categories.first
david.reload
assert ! david.categories.loaded?
assert_queries(1) do
assert david.categories.include?(category)
end
assert ! david.categories.loaded?
end
def test_has_many_through_include_returns_false_for_non_matching_record_to_verify_scoping
david = authors(:david)
category = Category.create!(:name => 'Not Associated')
assert ! david.categories.loaded?
assert ! david.categories.include?(category)
end
private
# create dynamic Post models to allow different dependency options
def find_post_with_dependency(post_id, association, association_name, dependency)
class_name = "PostWith#{association.to_s.classify}#{dependency.to_s.classify}"
Post.find(post_id).update_attribute :type, class_name
klass = Object.const_set(class_name, Class.new(ActiveRecord::Base))
klass.set_table_name 'posts'
klass.send(association, association_name, :as => :taggable, :dependent => dependency)
klass.find(post_id)
end
end

View file

@ -1,262 +0,0 @@
require "cases/helper"
require 'models/developer'
require 'models/project'
require 'models/company'
require 'models/topic'
require 'models/reply'
require 'models/computer'
require 'models/customer'
require 'models/order'
require 'models/categorization'
require 'models/category'
require 'models/post'
require 'models/author'
require 'models/comment'
require 'models/tag'
require 'models/tagging'
require 'models/person'
require 'models/reader'
require 'models/parrot'
require 'models/pirate'
require 'models/treasure'
require 'models/price_estimate'
require 'models/club'
require 'models/member'
require 'models/membership'
require 'models/sponsor'
class AssociationsTest < ActiveRecord::TestCase
fixtures :accounts, :companies, :developers, :projects, :developers_projects,
:computers
def test_include_with_order_works
assert_nothing_raised {Account.find(:first, :order => 'id', :include => :firm)}
assert_nothing_raised {Account.find(:first, :order => :id, :include => :firm)}
end
def test_bad_collection_keys
assert_raise(ArgumentError, 'ActiveRecord should have barked on bad collection keys') do
Class.new(ActiveRecord::Base).has_many(:wheels, :name => 'wheels')
end
end
def test_should_construct_new_finder_sql_after_create
person = Person.new :first_name => 'clark'
assert_equal [], person.readers.find(:all)
person.save!
reader = Reader.create! :person => person, :post => Post.new(:title => "foo", :body => "bar")
assert_equal [reader], person.readers.find(:all)
end
def test_force_reload
firm = Firm.new("name" => "A New Firm, Inc")
firm.save
firm.clients.each {|c|} # forcing to load all clients
assert firm.clients.empty?, "New firm shouldn't have client objects"
assert_equal 0, firm.clients.size, "New firm should have 0 clients"
client = Client.new("name" => "TheClient.com", "firm_id" => firm.id)
client.save
assert firm.clients.empty?, "New firm should have cached no client objects"
assert_equal 0, firm.clients.size, "New firm should have cached 0 clients count"
assert !firm.clients(true).empty?, "New firm should have reloaded client objects"
assert_equal 1, firm.clients(true).size, "New firm should have reloaded clients count"
end
def test_storing_in_pstore
require "tmpdir"
store_filename = File.join(Dir.tmpdir, "ar-pstore-association-test")
File.delete(store_filename) if File.exist?(store_filename)
require "pstore"
apple = Firm.create("name" => "Apple")
natural = Client.new("name" => "Natural Company")
apple.clients << natural
db = PStore.new(store_filename)
db.transaction do
db["apple"] = apple
end
db = PStore.new(store_filename)
db.transaction do
assert_equal "Natural Company", db["apple"].clients.first.name
end
end
end
class AssociationProxyTest < ActiveRecord::TestCase
fixtures :authors, :posts, :categorizations, :categories, :developers, :projects, :developers_projects
def test_proxy_accessors
welcome = posts(:welcome)
assert_equal welcome, welcome.author.proxy_owner
assert_equal welcome.class.reflect_on_association(:author), welcome.author.proxy_reflection
welcome.author.class # force load target
assert_equal welcome.author, welcome.author.proxy_target
david = authors(:david)
assert_equal david, david.posts.proxy_owner
assert_equal david.class.reflect_on_association(:posts), david.posts.proxy_reflection
david.posts.class # force load target
assert_equal david.posts, david.posts.proxy_target
assert_equal david, david.posts_with_extension.testing_proxy_owner
assert_equal david.class.reflect_on_association(:posts_with_extension), david.posts_with_extension.testing_proxy_reflection
david.posts_with_extension.class # force load target
assert_equal david.posts_with_extension, david.posts_with_extension.testing_proxy_target
end
def test_push_does_not_load_target
david = authors(:david)
david.posts << (post = Post.new(:title => "New on Edge", :body => "More cool stuff!"))
assert !david.posts.loaded?
assert david.posts.include?(post)
end
def test_push_has_many_through_does_not_load_target
david = authors(:david)
david.categories << categories(:technology)
assert !david.categories.loaded?
assert david.categories.include?(categories(:technology))
end
def test_push_followed_by_save_does_not_load_target
david = authors(:david)
david.posts << (post = Post.new(:title => "New on Edge", :body => "More cool stuff!"))
assert !david.posts.loaded?
david.save
assert !david.posts.loaded?
assert david.posts.include?(post)
end
def test_push_does_not_lose_additions_to_new_record
josh = Author.new(:name => "Josh")
josh.posts << Post.new(:title => "New on Edge", :body => "More cool stuff!")
assert josh.posts.loaded?
assert_equal 1, josh.posts.size
end
def test_save_on_parent_does_not_load_target
david = developers(:david)
assert !david.projects.loaded?
david.update_attribute(:created_at, Time.now)
assert !david.projects.loaded?
end
def test_inspect_does_not_reload_a_not_yet_loaded_target
andreas = Developer.new :name => 'Andreas', :log => 'new developer added'
assert !andreas.audit_logs.loaded?
assert_match(/message: "new developer added"/, andreas.audit_logs.inspect)
end
def test_save_on_parent_saves_children
developer = Developer.create :name => "Bryan", :salary => 50_000
assert_equal 1, developer.reload.audit_logs.size
end
def test_create_via_association_with_block
post = authors(:david).posts.create(:title => "New on Edge") {|p| p.body = "More cool stuff!"}
assert_equal post.title, "New on Edge"
assert_equal post.body, "More cool stuff!"
end
def test_create_with_bang_via_association_with_block
post = authors(:david).posts.create!(:title => "New on Edge") {|p| p.body = "More cool stuff!"}
assert_equal post.title, "New on Edge"
assert_equal post.body, "More cool stuff!"
end
def test_failed_reload_returns_nil
p = setup_dangling_association
assert_nil p.author.reload
end
def test_failed_reset_returns_nil
p = setup_dangling_association
assert_nil p.author.reset
end
def test_reload_returns_assocition
david = developers(:david)
assert_nothing_raised do
assert_equal david.projects, david.projects.reload.reload
end
end
def setup_dangling_association
josh = Author.create(:name => "Josh")
p = Post.create(:title => "New on Edge", :body => "More cool stuff!", :author => josh)
josh.destroy
p
end
end
class OverridingAssociationsTest < ActiveRecord::TestCase
class Person < ActiveRecord::Base; end
class DifferentPerson < ActiveRecord::Base; end
class PeopleList < ActiveRecord::Base
has_and_belongs_to_many :has_and_belongs_to_many, :before_add => :enlist
has_many :has_many, :before_add => :enlist
belongs_to :belongs_to
has_one :has_one
end
class DifferentPeopleList < PeopleList
# Different association with the same name, callbacks should be omitted here.
has_and_belongs_to_many :has_and_belongs_to_many, :class_name => 'DifferentPerson'
has_many :has_many, :class_name => 'DifferentPerson'
belongs_to :belongs_to, :class_name => 'DifferentPerson'
has_one :has_one, :class_name => 'DifferentPerson'
end
def test_habtm_association_redefinition_callbacks_should_differ_and_not_inherited
# redeclared association on AR descendant should not inherit callbacks from superclass
callbacks = PeopleList.read_inheritable_attribute(:before_add_for_has_and_belongs_to_many)
assert_equal([:enlist], callbacks)
callbacks = DifferentPeopleList.read_inheritable_attribute(:before_add_for_has_and_belongs_to_many)
assert_equal([], callbacks)
end
def test_has_many_association_redefinition_callbacks_should_differ_and_not_inherited
# redeclared association on AR descendant should not inherit callbacks from superclass
callbacks = PeopleList.read_inheritable_attribute(:before_add_for_has_many)
assert_equal([:enlist], callbacks)
callbacks = DifferentPeopleList.read_inheritable_attribute(:before_add_for_has_many)
assert_equal([], callbacks)
end
def test_habtm_association_redefinition_reflections_should_differ_and_not_inherited
assert_not_equal(
PeopleList.reflect_on_association(:has_and_belongs_to_many),
DifferentPeopleList.reflect_on_association(:has_and_belongs_to_many)
)
end
def test_has_many_association_redefinition_reflections_should_differ_and_not_inherited
assert_not_equal(
PeopleList.reflect_on_association(:has_many),
DifferentPeopleList.reflect_on_association(:has_many)
)
end
def test_belongs_to_association_redefinition_reflections_should_differ_and_not_inherited
assert_not_equal(
PeopleList.reflect_on_association(:belongs_to),
DifferentPeopleList.reflect_on_association(:belongs_to)
)
end
def test_has_one_association_redefinition_reflections_should_differ_and_not_inherited
assert_not_equal(
PeopleList.reflect_on_association(:has_one),
DifferentPeopleList.reflect_on_association(:has_one)
)
end
end

View file

@ -1,238 +0,0 @@
require "cases/helper"
require 'models/topic'
class AttributeMethodsTest < ActiveRecord::TestCase
fixtures :topics
def setup
@old_suffixes = ActiveRecord::Base.send(:attribute_method_suffixes).dup
@target = Class.new(ActiveRecord::Base)
@target.table_name = 'topics'
end
def teardown
ActiveRecord::Base.send(:attribute_method_suffixes).clear
ActiveRecord::Base.attribute_method_suffix *@old_suffixes
end
def test_match_attribute_method_query_returns_match_data
assert_not_nil md = @target.match_attribute_method?('title=')
assert_equal 'title', md.pre_match
assert_equal ['='], md.captures
%w(_hello_world ist! _maybe?).each do |suffix|
@target.class_eval "def attribute#{suffix}(*args) args end"
@target.attribute_method_suffix suffix
assert_not_nil md = @target.match_attribute_method?("title#{suffix}")
assert_equal 'title', md.pre_match
assert_equal [suffix], md.captures
end
end
def test_declared_attribute_method_affects_respond_to_and_method_missing
topic = @target.new(:title => 'Budget')
assert topic.respond_to?('title')
assert_equal 'Budget', topic.title
assert !topic.respond_to?('title_hello_world')
assert_raise(NoMethodError) { topic.title_hello_world }
%w(_hello_world _it! _candidate= able?).each do |suffix|
@target.class_eval "def attribute#{suffix}(*args) args end"
@target.attribute_method_suffix suffix
meth = "title#{suffix}"
assert topic.respond_to?(meth)
assert_equal ['title'], topic.send(meth)
assert_equal ['title', 'a'], topic.send(meth, 'a')
assert_equal ['title', 1, 2, 3], topic.send(meth, 1, 2, 3)
end
end
def test_should_unserialize_attributes_for_frozen_records
myobj = {:value1 => :value2}
topic = Topic.create("content" => myobj)
topic.freeze
assert_equal myobj, topic.content
end
def test_kernel_methods_not_implemented_in_activerecord
%w(test name display y).each do |method|
assert_equal false, ActiveRecord::Base.instance_method_already_implemented?(method), "##{method} is defined"
end
end
def test_primary_key_implemented
assert_equal true, Class.new(ActiveRecord::Base).instance_method_already_implemented?('id')
end
def test_defined_kernel_methods_implemented_in_model
%w(test name display y).each do |method|
klass = Class.new ActiveRecord::Base
klass.class_eval "def #{method}() 'defined #{method}' end"
assert_equal true, klass.instance_method_already_implemented?(method), "##{method} is not defined"
end
end
def test_defined_kernel_methods_implemented_in_model_abstract_subclass
%w(test name display y).each do |method|
abstract = Class.new ActiveRecord::Base
abstract.class_eval "def #{method}() 'defined #{method}' end"
abstract.abstract_class = true
klass = Class.new abstract
assert_equal true, klass.instance_method_already_implemented?(method), "##{method} is not defined"
end
end
def test_raises_dangerous_attribute_error_when_defining_activerecord_method_in_model
%w(save create_or_update).each do |method|
klass = Class.new ActiveRecord::Base
klass.class_eval "def #{method}() 'defined #{method}' end"
assert_raises ActiveRecord::DangerousAttributeError do
klass.instance_method_already_implemented?(method)
end
end
end
def test_only_time_related_columns_are_meant_to_be_cached_by_default
expected = %w(datetime timestamp time date).sort
assert_equal expected, ActiveRecord::Base.attribute_types_cached_by_default.map(&:to_s).sort
end
def test_declaring_attributes_as_cached_adds_them_to_the_attributes_cached_by_default
default_attributes = Topic.cached_attributes
Topic.cache_attributes :replies_count
expected = default_attributes + ["replies_count"]
assert_equal expected.sort, Topic.cached_attributes.sort
Topic.instance_variable_set "@cached_attributes", nil
end
def test_time_related_columns_are_actually_cached
column_types = %w(datetime timestamp time date).map(&:to_sym)
column_names = Topic.columns.select{|c| column_types.include?(c.type) }.map(&:name)
assert_equal column_names.sort, Topic.cached_attributes.sort
assert_equal time_related_columns_on_topic.sort, Topic.cached_attributes.sort
end
def test_accessing_cached_attributes_caches_the_converted_values_and_nothing_else
t = topics(:first)
cache = t.instance_variable_get "@attributes_cache"
assert_not_nil cache
assert cache.empty?
all_columns = Topic.columns.map(&:name)
cached_columns = time_related_columns_on_topic
uncached_columns = all_columns - cached_columns
all_columns.each do |attr_name|
attribute_gets_cached = Topic.cache_attribute?(attr_name)
val = t.send attr_name unless attr_name == "type"
if attribute_gets_cached
assert cached_columns.include?(attr_name)
assert_equal val, cache[attr_name]
else
assert uncached_columns.include?(attr_name)
assert !cache.include?(attr_name)
end
end
end
def test_time_attributes_are_retrieved_in_current_time_zone
in_time_zone "Pacific Time (US & Canada)" do
utc_time = Time.utc(2008, 1, 1)
record = @target.new
record[:written_on] = utc_time
assert_equal utc_time, record.written_on # record.written on is equal to (i.e., simultaneous with) utc_time
assert_kind_of ActiveSupport::TimeWithZone, record.written_on # but is a TimeWithZone
assert_equal TimeZone["Pacific Time (US & Canada)"], record.written_on.time_zone # and is in the current Time.zone
assert_equal Time.utc(2007, 12, 31, 16), record.written_on.time # and represents time values adjusted accordingly
end
end
def test_setting_time_zone_aware_attribute_to_utc
in_time_zone "Pacific Time (US & Canada)" do
utc_time = Time.utc(2008, 1, 1)
record = @target.new
record.written_on = utc_time
assert_equal utc_time, record.written_on
assert_equal TimeZone["Pacific Time (US & Canada)"], record.written_on.time_zone
assert_equal Time.utc(2007, 12, 31, 16), record.written_on.time
end
end
def test_setting_time_zone_aware_attribute_in_other_time_zone
utc_time = Time.utc(2008, 1, 1)
cst_time = utc_time.in_time_zone("Central Time (US & Canada)")
in_time_zone "Pacific Time (US & Canada)" do
record = @target.new
record.written_on = cst_time
assert_equal utc_time, record.written_on
assert_equal TimeZone["Pacific Time (US & Canada)"], record.written_on.time_zone
assert_equal Time.utc(2007, 12, 31, 16), record.written_on.time
end
end
def test_setting_time_zone_aware_attribute_with_string
utc_time = Time.utc(2008, 1, 1)
(-11..13).each do |timezone_offset|
time_string = utc_time.in_time_zone(timezone_offset).to_s
in_time_zone "Pacific Time (US & Canada)" do
record = @target.new
record.written_on = time_string
assert_equal Time.zone.parse(time_string), record.written_on
assert_equal TimeZone["Pacific Time (US & Canada)"], record.written_on.time_zone
assert_equal Time.utc(2007, 12, 31, 16), record.written_on.time
end
end
end
def test_setting_time_zone_aware_attribute_to_blank_string_returns_nil
in_time_zone "Pacific Time (US & Canada)" do
record = @target.new
record.written_on = ' '
assert_nil record.written_on
end
end
def test_setting_time_zone_aware_attribute_interprets_time_zone_unaware_string_in_time_zone
time_string = 'Tue Jan 01 00:00:00 2008'
(-11..13).each do |timezone_offset|
in_time_zone timezone_offset do
record = @target.new
record.written_on = time_string
assert_equal Time.zone.parse(time_string), record.written_on
assert_equal TimeZone[timezone_offset], record.written_on.time_zone
assert_equal Time.utc(2008, 1, 1), record.written_on.time
end
end
end
def test_setting_time_zone_aware_attribute_in_current_time_zone
utc_time = Time.utc(2008, 1, 1)
in_time_zone "Pacific Time (US & Canada)" do
record = @target.new
record.written_on = utc_time.in_time_zone
assert_equal utc_time, record.written_on
assert_equal TimeZone["Pacific Time (US & Canada)"], record.written_on.time_zone
assert_equal Time.utc(2007, 12, 31, 16), record.written_on.time
end
end
private
def time_related_columns_on_topic
Topic.columns.select{|c| [:time, :date, :datetime, :timestamp].include?(c.type)}.map(&:name)
end
def in_time_zone(zone)
old_zone = Time.zone
old_tz = ActiveRecord::Base.time_zone_aware_attributes
Time.zone = zone ? TimeZone[zone] : nil
ActiveRecord::Base.time_zone_aware_attributes = !zone.nil?
yield
ensure
Time.zone = old_zone
ActiveRecord::Base.time_zone_aware_attributes = old_tz
end
end

File diff suppressed because it is too large Load diff

View file

@ -1,34 +0,0 @@
require "cases/helper"
# Without using prepared statements, it makes no sense to test
# BLOB data with SQL Server, because the length of a statement is
# limited to 8KB.
#
# Without using prepared statements, it makes no sense to test
# BLOB data with DB2 or Firebird, because the length of a statement
# is limited to 32KB.
unless current_adapter?(:SQLServerAdapter, :SybaseAdapter, :DB2Adapter, :FirebirdAdapter)
require 'models/binary'
class BinaryTest < ActiveRecord::TestCase
FIXTURES = %w(flowers.jpg example.log)
def test_load_save
Binary.delete_all
FIXTURES.each do |filename|
data = File.read(ASSETS_ROOT + "/#{filename}")
data.force_encoding('ASCII-8BIT') if data.respond_to?(:force_encoding)
data.freeze
bin = Binary.new(:data => data)
assert_equal data, bin.data, 'Newly assigned data differs from original'
bin.save!
assert_equal data, bin.data, 'Data differs from original after save'
assert_equal data, bin.reload.data, 'Reloaded data differs from original'
end
end
end
end

View file

@ -1,271 +0,0 @@
require "cases/helper"
require 'models/company'
require 'models/topic'
Company.has_many :accounts
class NumericData < ActiveRecord::Base
self.table_name = 'numeric_data'
end
class CalculationsTest < ActiveRecord::TestCase
fixtures :companies, :accounts, :topics
def test_should_sum_field
assert_equal 318, Account.sum(:credit_limit)
end
def test_should_average_field
value = Account.average(:credit_limit)
assert_kind_of Float, value
assert_in_delta 53.0, value, 0.001
end
def test_should_return_nil_as_average
assert_nil NumericData.average(:bank_balance)
end
def test_should_get_maximum_of_field
assert_equal 60, Account.maximum(:credit_limit)
end
def test_should_get_maximum_of_field_with_include
assert_equal 50, Account.maximum(:credit_limit, :include => :firm, :conditions => "companies.name != 'Summit'")
end
def test_should_get_maximum_of_field_with_scoped_include
Account.with_scope :find => { :include => :firm, :conditions => "companies.name != 'Summit'" } do
assert_equal 50, Account.maximum(:credit_limit)
end
end
def test_should_get_minimum_of_field
assert_equal 50, Account.minimum(:credit_limit)
end
def test_should_group_by_field
c = Account.sum(:credit_limit, :group => :firm_id)
[1,6,2].each { |firm_id| assert c.keys.include?(firm_id) }
end
def test_should_group_by_summed_field
c = Account.sum(:credit_limit, :group => :firm_id)
assert_equal 50, c[1]
assert_equal 105, c[6]
assert_equal 60, c[2]
end
def test_should_order_by_grouped_field
c = Account.sum(:credit_limit, :group => :firm_id, :order => "firm_id")
assert_equal [1, 2, 6, 9], c.keys.compact
end
def test_should_order_by_calculation
c = Account.sum(:credit_limit, :group => :firm_id, :order => "sum_credit_limit desc, firm_id")
assert_equal [105, 60, 53, 50, 50], c.keys.collect { |k| c[k] }
assert_equal [6, 2, 9, 1], c.keys.compact
end
def test_should_limit_calculation
c = Account.sum(:credit_limit, :conditions => "firm_id IS NOT NULL",
:group => :firm_id, :order => "firm_id", :limit => 2)
assert_equal [1, 2], c.keys.compact
end
def test_should_limit_calculation_with_offset
c = Account.sum(:credit_limit, :conditions => "firm_id IS NOT NULL",
:group => :firm_id, :order => "firm_id", :limit => 2, :offset => 1)
assert_equal [2, 6], c.keys.compact
end
def test_should_group_by_summed_field_having_condition
c = Account.sum(:credit_limit, :group => :firm_id,
:having => 'sum(credit_limit) > 50')
assert_nil c[1]
assert_equal 105, c[6]
assert_equal 60, c[2]
end
def test_should_group_by_summed_association
c = Account.sum(:credit_limit, :group => :firm)
assert_equal 50, c[companies(:first_firm)]
assert_equal 105, c[companies(:rails_core)]
assert_equal 60, c[companies(:first_client)]
end
def test_should_sum_field_with_conditions
assert_equal 105, Account.sum(:credit_limit, :conditions => 'firm_id = 6')
end
def test_should_return_zero_if_sum_conditions_return_nothing
assert_equal 0, Account.sum(:credit_limit, :conditions => '1 = 2')
end
def test_should_group_by_summed_field_with_conditions
c = Account.sum(:credit_limit, :conditions => 'firm_id > 1',
:group => :firm_id)
assert_nil c[1]
assert_equal 105, c[6]
assert_equal 60, c[2]
end
def test_should_group_by_summed_field_with_conditions_and_having
c = Account.sum(:credit_limit, :conditions => 'firm_id > 1',
:group => :firm_id,
:having => 'sum(credit_limit) > 60')
assert_nil c[1]
assert_equal 105, c[6]
assert_nil c[2]
end
def test_should_group_by_fields_with_table_alias
c = Account.sum(:credit_limit, :group => 'accounts.firm_id')
assert_equal 50, c[1]
assert_equal 105, c[6]
assert_equal 60, c[2]
end
def test_should_calculate_with_invalid_field
assert_equal 6, Account.calculate(:count, '*')
assert_equal 6, Account.calculate(:count, :all)
end
def test_should_calculate_grouped_with_invalid_field
c = Account.count(:all, :group => 'accounts.firm_id')
assert_equal 1, c[1]
assert_equal 2, c[6]
assert_equal 1, c[2]
end
def test_should_calculate_grouped_association_with_invalid_field
c = Account.count(:all, :group => :firm)
assert_equal 1, c[companies(:first_firm)]
assert_equal 2, c[companies(:rails_core)]
assert_equal 1, c[companies(:first_client)]
end
uses_mocha 'group_by_non_numeric_foreign_key_association' do
def test_should_group_by_association_with_non_numeric_foreign_key
ActiveRecord::Base.connection.expects(:select_all).returns([{"count_all" => 1, "firm_id" => "ABC"}])
firm = mock()
firm.expects(:id).returns("ABC")
firm.expects(:class).returns(Firm)
Company.expects(:find).with(["ABC"]).returns([firm])
column = mock()
column.expects(:name).at_least_once.returns(:firm_id)
column.expects(:type_cast).with("ABC").returns("ABC")
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
end
end
def test_should_calculate_grouped_association_with_foreign_key_option
Account.belongs_to :another_firm, :class_name => 'Firm', :foreign_key => 'firm_id'
c = Account.count(:all, :group => :another_firm)
assert_equal 1, c[companies(:first_firm)]
assert_equal 2, c[companies(:rails_core)]
assert_equal 1, c[companies(:first_client)]
end
def test_should_not_modify_options_when_using_includes
options = {:conditions => 'companies.id > 1', :include => :firm}
options_copy = options.dup
Account.count(:all, options)
assert_equal options_copy, options
end
def test_should_calculate_grouped_by_function
c = Company.count(:all, :group => "UPPER(#{QUOTED_TYPE})")
assert_equal 2, c[nil]
assert_equal 1, c['DEPENDENTFIRM']
assert_equal 3, c['CLIENT']
assert_equal 2, c['FIRM']
end
def test_should_calculate_grouped_by_function_with_table_alias
c = Company.count(:all, :group => "UPPER(companies.#{QUOTED_TYPE})")
assert_equal 2, c[nil]
assert_equal 1, c['DEPENDENTFIRM']
assert_equal 3, c['CLIENT']
assert_equal 2, c['FIRM']
end
def test_should_not_overshadow_enumerable_sum
assert_equal 6, [1, 2, 3].sum(&:abs)
end
def test_should_sum_scoped_field
assert_equal 15, companies(:rails_core).companies.sum(:id)
end
def test_should_sum_scoped_field_with_conditions
assert_equal 8, companies(:rails_core).companies.sum(:id, :conditions => 'id > 7')
end
def test_should_group_by_scoped_field
c = companies(:rails_core).companies.sum(:id, :group => :name)
assert_equal 7, c['Leetsoft']
assert_equal 8, c['Jadedpixel']
end
def test_should_group_by_summed_field_with_conditions_and_having
c = companies(:rails_core).companies.sum(:id, :group => :name,
:having => 'sum(id) > 7')
assert_nil c['Leetsoft']
assert_equal 8, c['Jadedpixel']
end
def test_should_reject_invalid_options
assert_nothing_raised do
[:count, :sum].each do |func|
# empty options are valid
Company.send(:validate_calculation_options, func)
# these options are valid for all calculations
[:select, :conditions, :joins, :order, :group, :having, :distinct].each do |opt|
Company.send(:validate_calculation_options, func, opt => true)
end
end
# :include is only valid on :count
Company.send(:validate_calculation_options, :count, :include => true)
end
assert_raises(ArgumentError) { Company.send(:validate_calculation_options, :sum, :foo => :bar) }
assert_raises(ArgumentError) { Company.send(:validate_calculation_options, :count, :foo => :bar) }
end
def test_should_count_selected_field_with_include
assert_equal 6, Account.count(:distinct => true, :include => :firm)
assert_equal 4, Account.count(:distinct => true, :include => :firm, :select => :credit_limit)
end
def test_should_count_manual_select_with_include
assert_equal 6, Account.count(:select => "DISTINCT accounts.id", :include => :firm)
end
def test_count_with_column_parameter
assert_equal 5, Account.count(:firm_id)
end
def test_count_with_column_and_options_parameter
assert_equal 2, Account.count(:firm_id, :conditions => "credit_limit = 50")
end
def test_count_with_no_parameters_isnt_deprecated
assert_not_deprecated { Account.count }
end
def test_count_with_too_many_parameters_raises
assert_raise(ArgumentError) { Account.count(1, 2, 3) }
end
def test_should_sum_expression
assert_equal "636", Account.sum("2 * credit_limit")
end
end

View file

@ -1,400 +0,0 @@
require "cases/helper"
class CallbackDeveloper < ActiveRecord::Base
set_table_name 'developers'
class << self
def callback_string(callback_method)
"history << [#{callback_method.to_sym.inspect}, :string]"
end
def callback_proc(callback_method)
Proc.new { |model| model.history << [callback_method, :proc] }
end
def define_callback_method(callback_method)
define_method("#{callback_method}_method") do |model|
model.history << [callback_method, :method]
end
end
def callback_object(callback_method)
klass = Class.new
klass.send(:define_method, callback_method) do |model|
model.history << [callback_method, :object]
end
klass.new
end
end
ActiveRecord::Callbacks::CALLBACKS.each do |callback_method|
callback_method_sym = callback_method.to_sym
define_callback_method(callback_method_sym)
send(callback_method, callback_method_sym)
send(callback_method, callback_string(callback_method_sym))
send(callback_method, callback_proc(callback_method_sym))
send(callback_method, callback_object(callback_method_sym))
send(callback_method) { |model| model.history << [callback_method_sym, :block] }
end
def history
@history ||= []
end
# after_initialize and after_find are invoked only if instance methods have been defined.
def after_initialize
end
def after_find
end
end
class ParentDeveloper < ActiveRecord::Base
set_table_name 'developers'
attr_accessor :after_save_called
before_validation {|record| record.after_save_called = true}
end
class ChildDeveloper < ParentDeveloper
end
class RecursiveCallbackDeveloper < ActiveRecord::Base
set_table_name 'developers'
before_save :on_before_save
after_save :on_after_save
attr_reader :on_before_save_called, :on_after_save_called
def on_before_save
@on_before_save_called ||= 0
@on_before_save_called += 1
save unless @on_before_save_called > 1
end
def on_after_save
@on_after_save_called ||= 0
@on_after_save_called += 1
save unless @on_after_save_called > 1
end
end
class ImmutableDeveloper < ActiveRecord::Base
set_table_name 'developers'
validates_inclusion_of :salary, :in => 50000..200000
before_save :cancel
before_destroy :cancel
def cancelled?
@cancelled == true
end
private
def cancel
@cancelled = true
false
end
end
class ImmutableMethodDeveloper < ActiveRecord::Base
set_table_name 'developers'
validates_inclusion_of :salary, :in => 50000..200000
def cancelled?
@cancelled == true
end
def before_save
@cancelled = true
false
end
def before_destroy
@cancelled = true
false
end
end
class CallbackCancellationDeveloper < ActiveRecord::Base
set_table_name 'developers'
def before_create
false
end
end
class CallbacksTest < ActiveRecord::TestCase
fixtures :developers
def test_initialize
david = CallbackDeveloper.new
assert_equal [
[ :after_initialize, :string ],
[ :after_initialize, :proc ],
[ :after_initialize, :object ],
[ :after_initialize, :block ],
], david.history
end
def test_find
david = CallbackDeveloper.find(1)
assert_equal [
[ :after_find, :string ],
[ :after_find, :proc ],
[ :after_find, :object ],
[ :after_find, :block ],
[ :after_initialize, :string ],
[ :after_initialize, :proc ],
[ :after_initialize, :object ],
[ :after_initialize, :block ],
], david.history
end
def test_new_valid?
david = CallbackDeveloper.new
david.valid?
assert_equal [
[ :after_initialize, :string ],
[ :after_initialize, :proc ],
[ :after_initialize, :object ],
[ :after_initialize, :block ],
[ :before_validation, :string ],
[ :before_validation, :proc ],
[ :before_validation, :object ],
[ :before_validation, :block ],
[ :before_validation_on_create, :string ],
[ :before_validation_on_create, :proc ],
[ :before_validation_on_create, :object ],
[ :before_validation_on_create, :block ],
[ :after_validation, :string ],
[ :after_validation, :proc ],
[ :after_validation, :object ],
[ :after_validation, :block ],
[ :after_validation_on_create, :string ],
[ :after_validation_on_create, :proc ],
[ :after_validation_on_create, :object ],
[ :after_validation_on_create, :block ]
], david.history
end
def test_existing_valid?
david = CallbackDeveloper.find(1)
david.valid?
assert_equal [
[ :after_find, :string ],
[ :after_find, :proc ],
[ :after_find, :object ],
[ :after_find, :block ],
[ :after_initialize, :string ],
[ :after_initialize, :proc ],
[ :after_initialize, :object ],
[ :after_initialize, :block ],
[ :before_validation, :string ],
[ :before_validation, :proc ],
[ :before_validation, :object ],
[ :before_validation, :block ],
[ :before_validation_on_update, :string ],
[ :before_validation_on_update, :proc ],
[ :before_validation_on_update, :object ],
[ :before_validation_on_update, :block ],
[ :after_validation, :string ],
[ :after_validation, :proc ],
[ :after_validation, :object ],
[ :after_validation, :block ],
[ :after_validation_on_update, :string ],
[ :after_validation_on_update, :proc ],
[ :after_validation_on_update, :object ],
[ :after_validation_on_update, :block ]
], david.history
end
def test_create
david = CallbackDeveloper.create('name' => 'David', 'salary' => 1000000)
assert_equal [
[ :after_initialize, :string ],
[ :after_initialize, :proc ],
[ :after_initialize, :object ],
[ :after_initialize, :block ],
[ :before_validation, :string ],
[ :before_validation, :proc ],
[ :before_validation, :object ],
[ :before_validation, :block ],
[ :before_validation_on_create, :string ],
[ :before_validation_on_create, :proc ],
[ :before_validation_on_create, :object ],
[ :before_validation_on_create, :block ],
[ :after_validation, :string ],
[ :after_validation, :proc ],
[ :after_validation, :object ],
[ :after_validation, :block ],
[ :after_validation_on_create, :string ],
[ :after_validation_on_create, :proc ],
[ :after_validation_on_create, :object ],
[ :after_validation_on_create, :block ],
[ :before_save, :string ],
[ :before_save, :proc ],
[ :before_save, :object ],
[ :before_save, :block ],
[ :before_create, :string ],
[ :before_create, :proc ],
[ :before_create, :object ],
[ :before_create, :block ],
[ :after_create, :string ],
[ :after_create, :proc ],
[ :after_create, :object ],
[ :after_create, :block ],
[ :after_save, :string ],
[ :after_save, :proc ],
[ :after_save, :object ],
[ :after_save, :block ]
], david.history
end
def test_save
david = CallbackDeveloper.find(1)
david.save
assert_equal [
[ :after_find, :string ],
[ :after_find, :proc ],
[ :after_find, :object ],
[ :after_find, :block ],
[ :after_initialize, :string ],
[ :after_initialize, :proc ],
[ :after_initialize, :object ],
[ :after_initialize, :block ],
[ :before_validation, :string ],
[ :before_validation, :proc ],
[ :before_validation, :object ],
[ :before_validation, :block ],
[ :before_validation_on_update, :string ],
[ :before_validation_on_update, :proc ],
[ :before_validation_on_update, :object ],
[ :before_validation_on_update, :block ],
[ :after_validation, :string ],
[ :after_validation, :proc ],
[ :after_validation, :object ],
[ :after_validation, :block ],
[ :after_validation_on_update, :string ],
[ :after_validation_on_update, :proc ],
[ :after_validation_on_update, :object ],
[ :after_validation_on_update, :block ],
[ :before_save, :string ],
[ :before_save, :proc ],
[ :before_save, :object ],
[ :before_save, :block ],
[ :before_update, :string ],
[ :before_update, :proc ],
[ :before_update, :object ],
[ :before_update, :block ],
[ :after_update, :string ],
[ :after_update, :proc ],
[ :after_update, :object ],
[ :after_update, :block ],
[ :after_save, :string ],
[ :after_save, :proc ],
[ :after_save, :object ],
[ :after_save, :block ]
], david.history
end
def test_destroy
david = CallbackDeveloper.find(1)
david.destroy
assert_equal [
[ :after_find, :string ],
[ :after_find, :proc ],
[ :after_find, :object ],
[ :after_find, :block ],
[ :after_initialize, :string ],
[ :after_initialize, :proc ],
[ :after_initialize, :object ],
[ :after_initialize, :block ],
[ :before_destroy, :string ],
[ :before_destroy, :proc ],
[ :before_destroy, :object ],
[ :before_destroy, :block ],
[ :after_destroy, :string ],
[ :after_destroy, :proc ],
[ :after_destroy, :object ],
[ :after_destroy, :block ]
], david.history
end
def test_delete
david = CallbackDeveloper.find(1)
CallbackDeveloper.delete(david.id)
assert_equal [
[ :after_find, :string ],
[ :after_find, :proc ],
[ :after_find, :object ],
[ :after_find, :block ],
[ :after_initialize, :string ],
[ :after_initialize, :proc ],
[ :after_initialize, :object ],
[ :after_initialize, :block ],
], david.history
end
def test_before_save_returning_false
david = ImmutableDeveloper.find(1)
assert david.valid?
assert !david.save
assert_raises(ActiveRecord::RecordNotSaved) { david.save! }
david = ImmutableDeveloper.find(1)
david.salary = 10_000_000
assert !david.valid?
assert !david.save
assert_raises(ActiveRecord::RecordInvalid) { david.save! }
end
def test_before_create_returning_false
someone = CallbackCancellationDeveloper.new
assert someone.valid?
assert !someone.save
end
def test_before_destroy_returning_false
david = ImmutableDeveloper.find(1)
assert !david.destroy
assert_not_nil ImmutableDeveloper.find_by_id(1)
end
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 }
CallbackDeveloper.before_validation proc { |model| model.history << [:before_validation, :should_never_get_here] }
david.save
assert_equal [
[ :after_find, :string ],
[ :after_find, :proc ],
[ :after_find, :object ],
[ :after_find, :block ],
[ :after_initialize, :string ],
[ :after_initialize, :proc ],
[ :after_initialize, :object ],
[ :after_initialize, :block ],
[ :before_validation, :string ],
[ :before_validation, :proc ],
[ :before_validation, :object ],
[ :before_validation, :block ],
[ :before_validation, :returning_false ]
], david.history
end
def test_inheritence_of_callbacks
parent = ParentDeveloper.new
assert !parent.after_save_called
parent.save
assert parent.after_save_called
child = ChildDeveloper.new
assert !child.after_save_called
child.save
assert child.after_save_called
end
end

View file

@ -1,32 +0,0 @@
require 'test/unit'
require "cases/helper"
require 'active_support/core_ext/class/inheritable_attributes'
class A
include ClassInheritableAttributes
end
class B < A
write_inheritable_array "first", [ :one, :two ]
end
class C < A
write_inheritable_array "first", [ :three ]
end
class D < B
write_inheritable_array "first", [ :four ]
end
class ClassInheritableAttributesTest < ActiveRecord::TestCase
def test_first_level
assert_equal [ :one, :two ], B.read_inheritable_attribute("first")
assert_equal [ :three ], C.read_inheritable_attribute("first")
end
def test_second_level
assert_equal [ :one, :two, :four ], D.read_inheritable_attribute("first")
assert_equal [ :one, :two ], B.read_inheritable_attribute("first")
end
end

View file

@ -1,17 +0,0 @@
require "cases/helper"
require 'models/topic'
class TestColumnAlias < ActiveRecord::TestCase
fixtures :topics
QUERY = if 'Oracle' == ActiveRecord::Base.connection.adapter_name
'SELECT id AS pk FROM topics WHERE ROWNUM < 2'
else
'SELECT id AS pk FROM topics'
end
def test_column_alias
records = Topic.connection.select_all(QUERY)
assert_equal 'pk', records[0].keys[0]
end
end

View file

@ -1,8 +0,0 @@
require "cases/helper"
class FirebirdConnectionTest < ActiveRecord::TestCase
def test_charset_properly_set
fb_conn = ActiveRecord::Base.connection.instance_variable_get(:@connection)
assert_equal 'UTF8', fb_conn.database.character_set
end
end

View file

@ -1,30 +0,0 @@
require "cases/helper"
class MysqlConnectionTest < ActiveRecord::TestCase
def setup
@connection = ActiveRecord::Base.connection
end
def test_no_automatic_reconnection_after_timeout
assert @connection.active?
@connection.update('set @@wait_timeout=1')
sleep 2
assert !@connection.active?
end
def test_successful_reconnection_after_timeout_with_manual_reconnect
assert @connection.active?
@connection.update('set @@wait_timeout=1')
sleep 2
@connection.reconnect!
assert @connection.active?
end
def test_successful_reconnection_after_timeout_with_verify
assert @connection.active?
@connection.update('set @@wait_timeout=1')
sleep 2
@connection.verify!(0)
assert @connection.active?
end
end

View file

@ -1,69 +0,0 @@
require "cases/helper"
class CopyTableTest < ActiveRecord::TestCase
fixtures :companies, :comments
def setup
@connection = ActiveRecord::Base.connection
class << @connection
public :copy_table, :table_structure, :indexes
end
end
def test_copy_table(from = 'companies', to = 'companies2', options = {})
assert_nothing_raised {copy_table(from, to, options)}
assert_equal row_count(from), row_count(to)
if block_given?
yield from, to, options
else
assert_equal column_names(from), column_names(to)
end
@connection.drop_table(to) rescue nil
end
def test_copy_table_renaming_column
test_copy_table('companies', 'companies2',
:rename => {'client_of' => 'fan_of'}) do |from, to, options|
expected = column_values(from, 'client_of')
assert expected.any?, 'only nils in resultset; real values are needed'
assert_equal expected, column_values(to, 'fan_of')
end
end
def test_copy_table_with_index
test_copy_table('comments', 'comments_with_index') do
@connection.add_index('comments_with_index', ['post_id', 'type'])
test_copy_table('comments_with_index', 'comments_with_index2') do
assert_equal table_indexes_without_name('comments_with_index'),
table_indexes_without_name('comments_with_index2')
end
end
end
def test_copy_table_without_primary_key
test_copy_table('developers_projects', 'programmers_projects')
end
protected
def copy_table(from, to, options = {})
@connection.copy_table(from, to, {:temporary => true}.merge(options))
end
def column_names(table)
@connection.table_structure(table).map {|column| column['name']}
end
def column_values(table, column)
@connection.select_all("SELECT #{column} FROM #{table} ORDER BY id").map {|row| row[column]}
end
def table_indexes_without_name(table)
@connection.indexes('comments_with_index').delete(:name)
end
def row_count(table)
@connection.select_one("SELECT COUNT(*) AS count FROM #{table}")['count']
end
end

View file

@ -1,203 +0,0 @@
require "cases/helper"
class PostgresqlArray < ActiveRecord::Base
end
class PostgresqlMoney < ActiveRecord::Base
end
class PostgresqlNumber < ActiveRecord::Base
end
class PostgresqlTime < ActiveRecord::Base
end
class PostgresqlNetworkAddress < ActiveRecord::Base
end
class PostgresqlBitString < ActiveRecord::Base
end
class PostgresqlOid < ActiveRecord::Base
end
class PostgresqlDataTypeTest < ActiveRecord::TestCase
self.use_transactional_fixtures = false
def setup
@connection = ActiveRecord::Base.connection
@connection.execute("INSERT INTO postgresql_arrays (commission_by_quarter, nicknames) VALUES ( '{35000,21000,18000,17000}', '{foo,bar,baz}' )")
@first_array = PostgresqlArray.find(1)
@connection.execute("INSERT INTO postgresql_moneys (wealth) VALUES ('567.89'::money)")
@connection.execute("INSERT INTO postgresql_moneys (wealth) VALUES ('-567.89'::money)")
@first_money = PostgresqlMoney.find(1)
@second_money = PostgresqlMoney.find(2)
@connection.execute("INSERT INTO postgresql_numbers (single, double) VALUES (123.456, 123456.789)")
@first_number = PostgresqlNumber.find(1)
@connection.execute("INSERT INTO postgresql_times (time_interval) VALUES ('1 year 2 days ago')")
@first_time = PostgresqlTime.find(1)
@connection.execute("INSERT INTO postgresql_network_addresses (cidr_address, inet_address, mac_address) VALUES('192.168.0/24', '172.16.1.254/32', '01:23:45:67:89:0a')")
@first_network_address = PostgresqlNetworkAddress.find(1)
@connection.execute("INSERT INTO postgresql_bit_strings (bit_string, bit_string_varying) VALUES (B'00010101', X'15')")
@first_bit_string = PostgresqlBitString.find(1)
@connection.execute("INSERT INTO postgresql_oids (obj_id) VALUES (1234)")
@first_oid = PostgresqlOid.find(1)
end
def test_data_type_of_array_types
assert_equal :string, @first_array.column_for_attribute(:commission_by_quarter).type
assert_equal :string, @first_array.column_for_attribute(:nicknames).type
end
def test_data_type_of_money_types
assert_equal :decimal, @first_money.column_for_attribute(:wealth).type
end
def test_data_type_of_number_types
assert_equal :float, @first_number.column_for_attribute(:single).type
assert_equal :float, @first_number.column_for_attribute(:double).type
end
def test_data_type_of_time_types
assert_equal :string, @first_time.column_for_attribute(:time_interval).type
end
def test_data_type_of_network_address_types
assert_equal :string, @first_network_address.column_for_attribute(:cidr_address).type
assert_equal :string, @first_network_address.column_for_attribute(:inet_address).type
assert_equal :string, @first_network_address.column_for_attribute(:mac_address).type
end
def test_data_type_of_bit_string_types
assert_equal :string, @first_bit_string.column_for_attribute(:bit_string).type
assert_equal :string, @first_bit_string.column_for_attribute(:bit_string_varying).type
end
def test_data_type_of_oid_types
assert_equal :integer, @first_oid.column_for_attribute(:obj_id).type
end
def test_array_values
assert_equal '{35000,21000,18000,17000}', @first_array.commission_by_quarter
assert_equal '{foo,bar,baz}', @first_array.nicknames
end
def test_money_values
assert_equal 567.89, @first_money.wealth
assert_equal -567.89, @second_money.wealth
end
def test_number_values
assert_equal 123.456, @first_number.single
assert_equal 123456.789, @first_number.double
end
def test_time_values
assert_equal '-1 years -2 days', @first_time.time_interval
end
def test_network_address_values
assert_equal '192.168.0.0/24', @first_network_address.cidr_address
assert_equal '172.16.1.254', @first_network_address.inet_address
assert_equal '01:23:45:67:89:0a', @first_network_address.mac_address
end
def test_bit_string_values
assert_equal '00010101', @first_bit_string.bit_string
assert_equal '00010101', @first_bit_string.bit_string_varying
end
def test_oid_values
assert_equal 1234, @first_oid.obj_id
end
def test_update_integer_array
new_value = '{32800,95000,29350,17000}'
assert @first_array.commission_by_quarter = new_value
assert @first_array.save
assert @first_array.reload
assert_equal @first_array.commission_by_quarter, new_value
assert @first_array.commission_by_quarter = new_value
assert @first_array.save
assert @first_array.reload
assert_equal @first_array.commission_by_quarter, new_value
end
def test_update_text_array
new_value = '{robby,robert,rob,robbie}'
assert @first_array.nicknames = new_value
assert @first_array.save
assert @first_array.reload
assert_equal @first_array.nicknames, new_value
assert @first_array.nicknames = new_value
assert @first_array.save
assert @first_array.reload
assert_equal @first_array.nicknames, new_value
end
def test_update_money
new_value = BigDecimal.new('123.45')
assert @first_money.wealth = new_value
assert @first_money.save
assert @first_money.reload
assert_equal new_value, @first_money.wealth
end
def test_update_number
new_single = 789.012
new_double = 789012.345
assert @first_number.single = new_single
assert @first_number.double = new_double
assert @first_number.save
assert @first_number.reload
assert_equal @first_number.single, new_single
assert_equal @first_number.double, new_double
end
def test_update_time
assert @first_time.time_interval = '2 years 3 minutes'
assert @first_time.save
assert @first_time.reload
assert_equal @first_time.time_interval, '2 years 00:03:00'
end
def test_update_network_address
new_cidr_address = '10.1.2.3/32'
new_inet_address = '10.0.0.0/8'
new_mac_address = 'bc:de:f0:12:34:56'
assert @first_network_address.cidr_address = new_cidr_address
assert @first_network_address.inet_address = new_inet_address
assert @first_network_address.mac_address = new_mac_address
assert @first_network_address.save
assert @first_network_address.reload
assert_equal @first_network_address.cidr_address, new_cidr_address
assert_equal @first_network_address.inet_address, new_inet_address
assert_equal @first_network_address.mac_address, new_mac_address
end
def test_update_bit_string
new_bit_string = '11111111'
new_bit_string_varying = 'FF'
assert @first_bit_string.bit_string = new_bit_string
assert @first_bit_string.bit_string_varying = new_bit_string_varying
assert @first_bit_string.save
assert @first_bit_string.reload
assert_equal @first_bit_string.bit_string, new_bit_string
assert_equal @first_bit_string.bit_string, @first_bit_string.bit_string_varying
end
def test_update_oid
new_value = 567890
assert @first_oid.obj_id = new_value
assert @first_oid.save
assert @first_oid.reload
assert_equal @first_oid.obj_id, new_value
end
end

View file

@ -1,37 +0,0 @@
require "cases/helper"
require 'models/topic'
require 'models/task'
class DateTimeTest < ActiveRecord::TestCase
def test_saves_both_date_and_time
time_values = [1807, 2, 10, 15, 30, 45]
now = DateTime.civil(*time_values)
task = Task.new
task.starting = now
task.save!
# check against Time.local_time, since some platforms will return a Time instead of a DateTime
assert_equal Time.local_time(*time_values), Task.find(task.id).starting
end
def test_assign_empty_date_time
task = Task.new
task.starting = ''
task.ending = nil
assert_nil task.starting
assert_nil task.ending
end
def test_assign_empty_date
topic = Topic.new
topic.last_read = ''
assert_nil topic.last_read
end
def test_assign_empty_time
topic = Topic.new
topic.bonus_time = ''
assert_nil topic.bonus_time
end
end

View file

@ -1,16 +0,0 @@
require "cases/helper"
require 'models/default'
class DefaultTest < ActiveRecord::TestCase
def test_default_timestamp
default = Default.new
assert_instance_of(Time, default.default_timestamp)
assert_equal(:datetime, default.column_for_attribute(:default_timestamp).type)
# Variance should be small; increase if required -- e.g., if test db is on
# remote host and clocks aren't synchronized.
t1 = Time.new
accepted_variance = 1.0
assert_in_delta(t1.to_f, default.default_timestamp.to_f, accepted_variance)
end
end

View file

@ -1,69 +0,0 @@
require "cases/helper"
require 'models/default'
require 'models/entrant'
class DefaultTest < ActiveRecord::TestCase
def test_nil_defaults_for_not_null_columns
column_defaults =
if current_adapter?(:MysqlAdapter) && Mysql.client_version < 50051
{ 'id' => nil, 'name' => '', 'course_id' => nil }
else
{ 'id' => nil, 'name' => nil, 'course_id' => nil }
end
column_defaults.each do |name, default|
column = Entrant.columns_hash[name]
assert !column.null, "#{name} column should be NOT NULL"
assert_equal default, column.default, "#{name} column should be DEFAULT #{default.inspect}"
end
end
if current_adapter?(:MysqlAdapter)
# 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
klass = Class.new(ActiveRecord::Base)
klass.table_name = 'test_integer_not_null_default_zero'
klass.connection.create_table klass.table_name do |t|
t.column :zero, :integer, :null => false, :default => 0
t.column :omit, :integer, :null => false
end
assert_equal 0, klass.columns_hash['zero'].default
assert !klass.columns_hash['zero'].null
# 0 in MySQL 4, nil in 5.
assert [0, nil].include?(klass.columns_hash['omit'].default)
assert !klass.columns_hash['omit'].null
assert_raise(ActiveRecord::StatementInvalid) { klass.create! }
assert_nothing_raised do
instance = klass.create!(:omit => 1)
assert_equal 0, instance.zero
assert_equal 1, instance.omit
end
ensure
klass.connection.drop_table(klass.table_name) rescue nil
end
end
if current_adapter?(:PostgreSQLAdapter, :SQLServerAdapter, :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

@ -1,30 +0,0 @@
require "cases/helper"
require 'models/entrant'
class DeprecatedFinderTest < ActiveRecord::TestCase
fixtures :entrants
def test_deprecated_find_all_was_removed
assert_raise(NoMethodError) { Entrant.find_all }
end
def test_deprecated_find_first_was_removed
assert_raise(NoMethodError) { Entrant.find_first }
end
def test_deprecated_find_on_conditions_was_removed
assert_raise(NoMethodError) { Entrant.find_on_conditions }
end
def test_count
assert_equal(0, Entrant.count(:conditions => "id > 3"))
assert_equal(1, Entrant.count(:conditions => ["id > ?", 2]))
assert_equal(2, Entrant.count(:conditions => ["id > ?", 1]))
end
def test_count_by_sql
assert_equal(0, Entrant.count_by_sql("SELECT COUNT(*) FROM entrants WHERE id > 3"))
assert_equal(1, Entrant.count_by_sql(["SELECT COUNT(*) FROM entrants WHERE id > ?", 2]))
assert_equal(2, Entrant.count_by_sql(["SELECT COUNT(*) FROM entrants WHERE id > ?", 1]))
end
end

View file

@ -1,163 +0,0 @@
require 'cases/helper'
require 'models/topic' # For booleans
require 'models/pirate' # For timestamps
require 'models/parrot'
class Pirate # Just reopening it, not defining it
attr_accessor :detected_changes_in_after_update # Boolean for if changes are detected
attr_accessor :changes_detected_in_after_update # Actual changes
after_update :check_changes
private
# after_save/update in sweepers, observers, and the model itself
# can end up checking dirty status and acting on the results
def check_changes
if self.changed?
self.detected_changes_in_after_update = true
self.changes_detected_in_after_update = self.changes
end
end
end
class DirtyTest < ActiveRecord::TestCase
def test_attribute_changes
# New record - no changes.
pirate = Pirate.new
assert !pirate.catchphrase_changed?
assert_nil pirate.catchphrase_change
# Change catchphrase.
pirate.catchphrase = 'arrr'
assert pirate.catchphrase_changed?
assert_nil pirate.catchphrase_was
assert_equal [nil, 'arrr'], pirate.catchphrase_change
# Saved - no changes.
pirate.save!
assert !pirate.catchphrase_changed?
assert_nil pirate.catchphrase_change
# Same value - no changes.
pirate.catchphrase = 'arrr'
assert !pirate.catchphrase_changed?
assert_nil pirate.catchphrase_change
end
def test_nullable_integer_not_marked_as_changed_if_new_value_is_blank
pirate = Pirate.new
["", nil].each do |value|
pirate.parrot_id = value
assert !pirate.parrot_id_changed?
assert_nil pirate.parrot_id_change
end
end
def test_object_should_be_changed_if_any_attribute_is_changed
pirate = Pirate.new
assert !pirate.changed?
assert_equal [], pirate.changed
assert_equal Hash.new, pirate.changes
pirate.catchphrase = 'arrr'
assert pirate.changed?
assert_nil pirate.catchphrase_was
assert_equal %w(catchphrase), pirate.changed
assert_equal({'catchphrase' => [nil, 'arrr']}, pirate.changes)
pirate.save
assert !pirate.changed?
assert_equal [], pirate.changed
assert_equal Hash.new, pirate.changes
end
def test_attribute_will_change!
pirate = Pirate.create!(:catchphrase => 'arr')
pirate.catchphrase << ' matey'
assert !pirate.catchphrase_changed?
assert pirate.catchphrase_will_change!
assert pirate.catchphrase_changed?
assert_equal ['arr matey', 'arr matey'], pirate.catchphrase_change
pirate.catchphrase << '!'
assert pirate.catchphrase_changed?
assert_equal ['arr matey', 'arr matey!'], pirate.catchphrase_change
end
def test_association_assignment_changes_foreign_key
pirate = Pirate.create!(:catchphrase => 'jarl')
pirate.parrot = Parrot.create!
assert pirate.changed?
assert_equal %w(parrot_id), pirate.changed
end
def test_attribute_should_be_compared_with_type_cast
topic = Topic.new
assert topic.approved?
assert !topic.approved_changed?
# Coming from web form.
params = {:topic => {:approved => 1}}
# In the controller.
topic.attributes = params[:topic]
assert topic.approved?
assert !topic.approved_changed?
end
def test_partial_update
pirate = Pirate.new(:catchphrase => 'foo')
old_updated_on = 1.hour.ago.beginning_of_day
with_partial_updates Pirate, false do
assert_queries(2) { 2.times { pirate.save! } }
Pirate.update_all({ :updated_on => old_updated_on }, :id => pirate.id)
end
with_partial_updates Pirate, true do
assert_queries(0) { 2.times { pirate.save! } }
assert_equal old_updated_on, pirate.reload.updated_on
assert_queries(1) { pirate.catchphrase = 'bar'; pirate.save! }
assert_not_equal old_updated_on, pirate.reload.updated_on
end
end
def test_changed_attributes_should_be_preserved_if_save_failure
pirate = Pirate.new
pirate.parrot_id = 1
assert !pirate.save
check_pirate_after_save_failure(pirate)
pirate = Pirate.new
pirate.parrot_id = 1
assert_raises(ActiveRecord::RecordInvalid) { pirate.save! }
check_pirate_after_save_failure(pirate)
end
def test_reload_should_clear_changed_attributes
pirate = Pirate.create!(:catchphrase => "shiver me timbers")
pirate.catchphrase = "*hic*"
assert pirate.changed?
pirate.reload
assert !pirate.changed?
end
private
def with_partial_updates(klass, on = true)
old = klass.partial_updates?
klass.partial_updates = on
yield
ensure
klass.partial_updates = old
end
def check_pirate_after_save_failure(pirate)
assert pirate.changed?
assert pirate.parrot_id_changed?
assert_equal %w(parrot_id), pirate.changed
assert_nil pirate.parrot_id_was
end
end

View file

@ -1,76 +0,0 @@
require "cases/helper"
require 'models/topic'
class FinderRespondToTest < ActiveRecord::TestCase
fixtures :topics
def test_should_preserve_normal_respond_to_behaviour_and_respond_to_newly_added_method
class << Topic; self; end.send(:define_method, :method_added_for_finder_respond_to_test) { }
assert Topic.respond_to?(:method_added_for_finder_respond_to_test)
ensure
class << Topic; self; end.send(:remove_method, :method_added_for_finder_respond_to_test)
end
def test_should_preserve_normal_respond_to_behaviour_and_respond_to_standard_object_method
assert Topic.respond_to?(:to_s)
end
def test_should_respond_to_find_by_one_attribute_before_caching
ensure_topic_method_is_not_cached(:find_by_title)
assert Topic.respond_to?(:find_by_title)
end
def test_should_respond_to_find_all_by_one_attribute
ensure_topic_method_is_not_cached(:find_all_by_title)
assert Topic.respond_to?(:find_all_by_title)
end
def test_should_respond_to_find_all_by_two_attributes
ensure_topic_method_is_not_cached(:find_all_by_title_and_author_name)
assert Topic.respond_to?(:find_all_by_title_and_author_name)
end
def test_should_respond_to_find_by_two_attributes
ensure_topic_method_is_not_cached(:find_by_title_and_author_name)
assert Topic.respond_to?(:find_by_title_and_author_name)
end
def test_should_respond_to_find_or_initialize_from_one_attribute
ensure_topic_method_is_not_cached(:find_or_initialize_by_title)
assert Topic.respond_to?(:find_or_initialize_by_title)
end
def test_should_respond_to_find_or_initialize_from_two_attributes
ensure_topic_method_is_not_cached(:find_or_initialize_by_title_and_author_name)
assert Topic.respond_to?(:find_or_initialize_by_title_and_author_name)
end
def test_should_respond_to_find_or_create_from_one_attribute
ensure_topic_method_is_not_cached(:find_or_create_by_title)
assert Topic.respond_to?(:find_or_create_by_title)
end
def test_should_respond_to_find_or_create_from_two_attributes
ensure_topic_method_is_not_cached(:find_or_create_by_title_and_author_name)
assert Topic.respond_to?(:find_or_create_by_title_and_author_name)
end
def test_should_not_respond_to_find_by_one_missing_attribute
assert !Topic.respond_to?(:find_by_undertitle)
end
def test_should_not_respond_to_find_by_invalid_method_syntax
assert !Topic.respond_to?(:fail_to_find_by_title)
assert !Topic.respond_to?(:find_by_title?)
assert !Topic.respond_to?(:fail_to_find_or_create_by_title)
assert !Topic.respond_to?(:find_or_create_by_title?)
end
private
def ensure_topic_method_is_not_cached(method_id)
class << Topic; self; end.send(:remove_method, method_id) if Topic.public_methods.any? { |m| m.to_s == method_id.to_s }
end
end

View file

@ -1,883 +0,0 @@
require "cases/helper"
require 'models/author'
require 'models/comment'
require 'models/company'
require 'models/topic'
require 'models/reply'
require 'models/entrant'
require 'models/developer'
require 'models/post'
require 'models/customer'
require 'models/job'
require 'models/categorization'
class FinderTest < ActiveRecord::TestCase
fixtures :companies, :topics, :entrants, :developers, :developers_projects, :posts, :comments, :accounts, :authors, :customers
def test_find
assert_equal(topics(:first).title, Topic.find(1).title)
end
# find should handle strings that come from URLs
# (example: Category.find(params[:id]))
def test_find_with_string
assert_equal(Topic.find(1).title,Topic.find("1").title)
end
def test_exists
assert Topic.exists?(1)
assert Topic.exists?("1")
assert Topic.exists?(:author_name => "David")
assert Topic.exists?(:author_name => "Mary", :approved => true)
assert Topic.exists?(["parent_id = ?", 1])
assert !Topic.exists?(45)
begin
assert !Topic.exists?("foo")
rescue ActiveRecord::StatementInvalid
# PostgreSQL complains about string comparison with integer field
rescue Exception
flunk
end
assert_raise(NoMethodError) { Topic.exists?([1,2]) }
end
def test_exists_with_aggregate_having_three_mappings
existing_address = customers(:david).address
assert Customer.exists?(:address => existing_address)
end
def test_exists_with_aggregate_having_three_mappings_with_one_difference
existing_address = customers(:david).address
assert !Customer.exists?(:address =>
Address.new(existing_address.street, existing_address.city, existing_address.country + "1"))
assert !Customer.exists?(:address =>
Address.new(existing_address.street, existing_address.city + "1", existing_address.country))
assert !Customer.exists?(:address =>
Address.new(existing_address.street + "1", existing_address.city, existing_address.country))
end
def test_find_by_array_of_one_id
assert_kind_of(Array, Topic.find([ 1 ]))
assert_equal(1, Topic.find([ 1 ]).length)
end
def test_find_by_ids
assert_equal 2, Topic.find(1, 2).size
assert_equal topics(:second).title, Topic.find([2]).first.title
end
def test_find_by_ids_with_limit_and_offset
assert_equal 2, Entrant.find([1,3,2], :limit => 2).size
assert_equal 1, Entrant.find([1,3,2], :limit => 3, :offset => 2).size
# Also test an edge case: If you have 11 results, and you set a
# limit of 3 and offset of 9, then you should find that there
# will be only 2 results, regardless of the limit.
devs = Developer.find :all
last_devs = Developer.find devs.map(&:id), :limit => 3, :offset => 9
assert_equal 2, last_devs.size
end
def test_find_an_empty_array
assert_equal [], Topic.find([])
end
def test_find_by_ids_missing_one
assert_raises(ActiveRecord::RecordNotFound) { Topic.find(1, 2, 45) }
end
def test_find_all_with_limit
entrants = Entrant.find(:all, :order => "id ASC", :limit => 2)
assert_equal(2, entrants.size)
assert_equal(entrants(:first).name, entrants.first.name)
end
def test_find_all_with_prepared_limit_and_offset
entrants = Entrant.find(:all, :order => "id ASC", :limit => 2, :offset => 1)
assert_equal(2, entrants.size)
assert_equal(entrants(:second).name, entrants.first.name)
entrants = Entrant.find(:all, :order => "id ASC", :limit => 2, :offset => 2)
assert_equal(1, entrants.size)
assert_equal(entrants(:third).name, entrants.first.name)
end
def test_find_all_with_limit_and_offset_and_multiple_orderings
developers = Developer.find(:all, :order => "salary ASC, id DESC", :limit => 3, :offset => 1)
assert_equal ["David", "fixture_10", "fixture_9"], developers.collect {|d| d.name}
end
def test_find_with_limit_and_condition
developers = Developer.find(:all, :order => "id DESC", :conditions => "salary = 100000", :limit => 3, :offset =>7)
assert_equal(1, developers.size)
assert_equal("fixture_3", developers.first.name)
end
def test_find_with_entire_select_statement
topics = Topic.find_by_sql "SELECT * FROM topics WHERE author_name = 'Mary'"
assert_equal(1, topics.size)
assert_equal(topics(:second).title, topics.first.title)
end
def test_find_with_prepared_select_statement
topics = Topic.find_by_sql ["SELECT * FROM topics WHERE author_name = ?", "Mary"]
assert_equal(1, topics.size)
assert_equal(topics(:second).title, topics.first.title)
end
def test_find_by_sql_with_sti_on_joined_table
accounts = Account.find_by_sql("SELECT * FROM accounts INNER JOIN companies ON companies.id = accounts.firm_id")
assert_equal [Account], accounts.collect(&:class).uniq
end
def test_find_first
first = Topic.find(:first, :conditions => "title = 'The First Topic'")
assert_equal(topics(:first).title, first.title)
end
def test_find_first_failing
first = Topic.find(:first, :conditions => "title = 'The First Topic!'")
assert_nil(first)
end
def test_first
assert_equal topics(:second).title, Topic.first(:conditions => "title = 'The Second Topic of the day'").title
end
def test_first_failing
assert_nil Topic.first(:conditions => "title = 'The Second Topic of the day!'")
end
def test_unexisting_record_exception_handling
assert_raises(ActiveRecord::RecordNotFound) {
Topic.find(1).parent
}
Topic.find(2).topic
end
def test_find_only_some_columns
topic = Topic.find(1, :select => "author_name")
assert_raises(ActiveRecord::MissingAttributeError) {topic.title}
assert_equal "David", topic.author_name
assert !topic.attribute_present?("title")
#assert !topic.respond_to?("title")
assert topic.attribute_present?("author_name")
assert topic.respond_to?("author_name")
end
def test_find_on_blank_conditions
[nil, " ", [], {}].each do |blank|
assert_nothing_raised { Topic.find(:first, :conditions => blank) }
end
end
def test_find_on_blank_bind_conditions
[ [""], ["",{}] ].each do |blank|
assert_nothing_raised { Topic.find(:first, :conditions => blank) }
end
end
def test_find_on_array_conditions
assert Topic.find(1, :conditions => ["approved = ?", false])
assert_raises(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => ["approved = ?", true]) }
end
def test_find_on_hash_conditions
assert Topic.find(1, :conditions => { :approved => false })
assert_raises(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { :approved => true }) }
end
def test_find_on_hash_conditions_with_explicit_table_name
assert Topic.find(1, :conditions => { 'topics.approved' => false })
assert_raises(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { 'topics.approved' => true }) }
end
def test_find_on_hash_conditions_with_explicit_table_name_and_aggregate
david = customers(:david)
assert Customer.find(david.id, :conditions => { 'customers.name' => david.name, :address => david.address })
assert_raises(ActiveRecord::RecordNotFound) {
Customer.find(david.id, :conditions => { 'customers.name' => david.name + "1", :address => david.address })
}
end
def test_find_on_association_proxy_conditions
assert_equal [1, 2, 3, 5, 6, 7, 8, 9, 10], Comment.find_all_by_post_id(authors(:david).posts).map(&:id).sort
end
def test_find_on_hash_conditions_with_range
assert_equal [1,2], Topic.find(:all, :conditions => { :id => 1..2 }).map(&:id).sort
assert_raises(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { :id => 2..3 }) }
end
def test_find_on_hash_conditions_with_multiple_ranges
assert_equal [1,2,3], Comment.find(:all, :conditions => { :id => 1..3, :post_id => 1..2 }).map(&:id).sort
assert_equal [1], Comment.find(:all, :conditions => { :id => 1..1, :post_id => 1..10 }).map(&:id).sort
end
def test_find_on_multiple_hash_conditions
assert Topic.find(1, :conditions => { :author_name => "David", :title => "The First Topic", :replies_count => 1, :approved => false })
assert_raises(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { :author_name => "David", :title => "The First Topic", :replies_count => 1, :approved => true }) }
assert_raises(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { :author_name => "David", :title => "HHC", :replies_count => 1, :approved => false }) }
assert_raises(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { :author_name => "David", :title => "The First Topic", :replies_count => 1, :approved => true }) }
end
def test_condition_interpolation
assert_kind_of Firm, Company.find(:first, :conditions => ["name = '%s'", "37signals"])
assert_nil Company.find(:first, :conditions => ["name = '%s'", "37signals!"])
assert_nil Company.find(:first, :conditions => ["name = '%s'", "37signals!' OR 1=1"])
assert_kind_of Time, Topic.find(:first, :conditions => ["id = %d", 1]).written_on
end
def test_condition_array_interpolation
assert_kind_of Firm, Company.find(:first, :conditions => ["name = '%s'", "37signals"])
assert_nil Company.find(:first, :conditions => ["name = '%s'", "37signals!"])
assert_nil Company.find(:first, :conditions => ["name = '%s'", "37signals!' OR 1=1"])
assert_kind_of Time, Topic.find(:first, :conditions => ["id = %d", 1]).written_on
end
def test_condition_hash_interpolation
assert_kind_of Firm, Company.find(:first, :conditions => { :name => "37signals"})
assert_nil Company.find(:first, :conditions => { :name => "37signals!"})
assert_kind_of Time, Topic.find(:first, :conditions => {:id => 1}).written_on
end
def test_hash_condition_find_malformed
assert_raises(ActiveRecord::StatementInvalid) {
Company.find(:first, :conditions => { :id => 2, :dhh => true })
}
end
def test_hash_condition_find_with_escaped_characters
Company.create("name" => "Ain't noth'n like' \#stuff")
assert Company.find(:first, :conditions => { :name => "Ain't noth'n like' \#stuff" })
end
def test_hash_condition_find_with_array
p1, p2 = Post.find(:all, :limit => 2, :order => 'id asc')
assert_equal [p1, p2], Post.find(:all, :conditions => { :id => [p1, p2] }, :order => 'id asc')
assert_equal [p1, p2], Post.find(:all, :conditions => { :id => [p1, p2.id] }, :order => 'id asc')
end
def test_hash_condition_find_with_nil
topic = Topic.find(:first, :conditions => { :last_read => nil } )
assert_not_nil topic
assert_nil topic.last_read
end
def test_hash_condition_find_with_aggregate_having_one_mapping
balance = customers(:david).balance
assert_kind_of Money, balance
found_customer = Customer.find(:first, :conditions => {:balance => balance})
assert_equal customers(:david), found_customer
end
def test_hash_condition_find_with_aggregate_attribute_having_same_name_as_field_and_key_value_being_aggregate
gps_location = customers(:david).gps_location
assert_kind_of GpsLocation, gps_location
found_customer = Customer.find(:first, :conditions => {:gps_location => gps_location})
assert_equal customers(:david), found_customer
end
def test_hash_condition_find_with_aggregate_having_one_mapping_and_key_value_being_attribute_value
balance = customers(:david).balance
assert_kind_of Money, balance
found_customer = Customer.find(:first, :conditions => {:balance => balance.amount})
assert_equal customers(:david), found_customer
end
def test_hash_condition_find_with_aggregate_attribute_having_same_name_as_field_and_key_value_being_attribute_value
gps_location = customers(:david).gps_location
assert_kind_of GpsLocation, gps_location
found_customer = Customer.find(:first, :conditions => {:gps_location => gps_location.gps_location})
assert_equal customers(:david), found_customer
end
def test_hash_condition_find_with_aggregate_having_three_mappings
address = customers(:david).address
assert_kind_of Address, address
found_customer = Customer.find(:first, :conditions => {:address => address})
assert_equal customers(:david), found_customer
end
def test_hash_condition_find_with_one_condition_being_aggregate_and_another_not
address = customers(:david).address
assert_kind_of Address, address
found_customer = Customer.find(:first, :conditions => {:address => address, :name => customers(:david).name})
assert_equal customers(:david), found_customer
end
def test_bind_variables
assert_kind_of Firm, Company.find(:first, :conditions => ["name = ?", "37signals"])
assert_nil Company.find(:first, :conditions => ["name = ?", "37signals!"])
assert_nil Company.find(:first, :conditions => ["name = ?", "37signals!' OR 1=1"])
assert_kind_of Time, Topic.find(:first, :conditions => ["id = ?", 1]).written_on
assert_raises(ActiveRecord::PreparedStatementInvalid) {
Company.find(:first, :conditions => ["id=? AND name = ?", 2])
}
assert_raises(ActiveRecord::PreparedStatementInvalid) {
Company.find(:first, :conditions => ["id=?", 2, 3, 4])
}
end
def test_bind_variables_with_quotes
Company.create("name" => "37signals' go'es agains")
assert Company.find(:first, :conditions => ["name = ?", "37signals' go'es agains"])
end
def test_named_bind_variables_with_quotes
Company.create("name" => "37signals' go'es agains")
assert Company.find(:first, :conditions => ["name = :name", {:name => "37signals' go'es agains"}])
end
def test_bind_arity
assert_nothing_raised { bind '' }
assert_raises(ActiveRecord::PreparedStatementInvalid) { bind '', 1 }
assert_raises(ActiveRecord::PreparedStatementInvalid) { bind '?' }
assert_nothing_raised { bind '?', 1 }
assert_raises(ActiveRecord::PreparedStatementInvalid) { bind '?', 1, 1 }
end
def test_named_bind_variables
assert_equal '1', bind(':a', :a => 1) # ' ruby-mode
assert_equal '1 1', bind(':a :a', :a => 1) # ' ruby-mode
assert_nothing_raised { bind("'+00:00'", :foo => "bar") }
assert_kind_of Firm, Company.find(:first, :conditions => ["name = :name", { :name => "37signals" }])
assert_nil Company.find(:first, :conditions => ["name = :name", { :name => "37signals!" }])
assert_nil Company.find(:first, :conditions => ["name = :name", { :name => "37signals!' OR 1=1" }])
assert_kind_of Time, Topic.find(:first, :conditions => ["id = :id", { :id => 1 }]).written_on
end
def test_bind_enumerable
quoted_abc = %(#{ActiveRecord::Base.connection.quote('a')},#{ActiveRecord::Base.connection.quote('b')},#{ActiveRecord::Base.connection.quote('c')})
assert_equal '1,2,3', bind('?', [1, 2, 3])
assert_equal quoted_abc, bind('?', %w(a b c))
assert_equal '1,2,3', bind(':a', :a => [1, 2, 3])
assert_equal quoted_abc, bind(':a', :a => %w(a b c)) # '
require 'set'
assert_equal '1,2,3', bind('?', Set.new([1, 2, 3]))
assert_equal quoted_abc, bind('?', Set.new(%w(a b c)))
assert_equal '1,2,3', bind(':a', :a => Set.new([1, 2, 3]))
assert_equal quoted_abc, bind(':a', :a => Set.new(%w(a b c))) # '
end
def test_bind_empty_enumerable
quoted_nil = ActiveRecord::Base.connection.quote(nil)
assert_equal quoted_nil, bind('?', [])
assert_equal " in (#{quoted_nil})", bind(' in (?)', [])
assert_equal "foo in (#{quoted_nil})", bind('foo in (?)', [])
end
def test_bind_string
assert_equal ActiveRecord::Base.connection.quote(''), bind('?', '')
end
def test_bind_record
o = Struct.new(:quoted_id).new(1)
assert_equal '1', bind('?', o)
os = [o] * 3
assert_equal '1,1,1', bind('?', os)
end
def test_string_sanitation
assert_not_equal "#{ActiveRecord::Base.connection.quoted_string_prefix}'something ' 1=1'", ActiveRecord::Base.sanitize("something ' 1=1")
assert_equal "#{ActiveRecord::Base.connection.quoted_string_prefix}'something; select table'", ActiveRecord::Base.sanitize("something; select table")
end
def test_count
assert_equal(0, Entrant.count(:conditions => "id > 3"))
assert_equal(1, Entrant.count(:conditions => ["id > ?", 2]))
assert_equal(2, Entrant.count(:conditions => ["id > ?", 1]))
end
def test_count_by_sql
assert_equal(0, Entrant.count_by_sql("SELECT COUNT(*) FROM entrants WHERE id > 3"))
assert_equal(1, Entrant.count_by_sql(["SELECT COUNT(*) FROM entrants WHERE id > ?", 2]))
assert_equal(2, Entrant.count_by_sql(["SELECT COUNT(*) FROM entrants WHERE id > ?", 1]))
end
def test_find_by_one_attribute
assert_equal topics(:first), Topic.find_by_title("The First Topic")
assert_nil Topic.find_by_title("The First Topic!")
end
def test_find_by_one_attribute_caches_dynamic_finder
# ensure this test can run independently of order
class << Topic; self; end.send(:remove_method, :find_by_title) if Topic.public_methods.any? { |m| m.to_s == 'find_by_title' }
assert !Topic.public_methods.any? { |m| m.to_s == 'find_by_title' }
t = Topic.find_by_title("The First Topic")
assert Topic.public_methods.any? { |m| m.to_s == 'find_by_title' }
end
def test_dynamic_finder_returns_same_results_after_caching
# ensure this test can run independently of order
class << Topic; self; end.send(:remove_method, :find_by_title) if Topic.public_method_defined?(:find_by_title)
t = Topic.find_by_title("The First Topic")
assert_equal t, Topic.find_by_title("The First Topic") # find_by_title has been cached
end
def test_find_by_one_attribute_with_order_option
assert_equal accounts(:signals37), Account.find_by_credit_limit(50, :order => 'id')
assert_equal accounts(:rails_core_account), Account.find_by_credit_limit(50, :order => 'id DESC')
end
def test_find_by_one_attribute_with_conditions
assert_equal accounts(:rails_core_account), Account.find_by_credit_limit(50, :conditions => ['firm_id = ?', 6])
end
def test_find_by_one_attribute_that_is_an_aggregate
address = customers(:david).address
assert_kind_of Address, address
found_customer = Customer.find_by_address(address)
assert_equal customers(:david), found_customer
end
def test_find_by_one_attribute_that_is_an_aggregate_with_one_attribute_difference
address = customers(:david).address
assert_kind_of Address, address
missing_address = Address.new(address.street, address.city, address.country + "1")
assert_nil Customer.find_by_address(missing_address)
missing_address = Address.new(address.street, address.city + "1", address.country)
assert_nil Customer.find_by_address(missing_address)
missing_address = Address.new(address.street + "1", address.city, address.country)
assert_nil Customer.find_by_address(missing_address)
end
def test_find_by_two_attributes_that_are_both_aggregates
balance = customers(:david).balance
address = customers(:david).address
assert_kind_of Money, balance
assert_kind_of Address, address
found_customer = Customer.find_by_balance_and_address(balance, address)
assert_equal customers(:david), found_customer
end
def test_find_by_two_attributes_with_one_being_an_aggregate
balance = customers(:david).balance
assert_kind_of Money, balance
found_customer = Customer.find_by_balance_and_name(balance, customers(:david).name)
assert_equal customers(:david), found_customer
end
def test_dynamic_finder_on_one_attribute_with_conditions_caches_method
# ensure this test can run independently of order
class << Account; self; end.send(:remove_method, :find_by_credit_limit) if Account.public_methods.any? { |m| m.to_s == 'find_by_credit_limit' }
assert !Account.public_methods.any? { |m| m.to_s == 'find_by_credit_limit' }
a = Account.find_by_credit_limit(50, :conditions => ['firm_id = ?', 6])
assert Account.public_methods.any? { |m| m.to_s == 'find_by_credit_limit' }
end
def test_dynamic_finder_on_one_attribute_with_conditions_returns_same_results_after_caching
# ensure this test can run independently of order
class << Account; self; end.send(:remove_method, :find_by_credit_limit) if Account.public_methods.any? { |m| m.to_s == 'find_by_credit_limit' }
a = Account.find_by_credit_limit(50, :conditions => ['firm_id = ?', 6])
assert_equal a, Account.find_by_credit_limit(50, :conditions => ['firm_id = ?', 6]) # find_by_credit_limit has been cached
end
def test_find_by_one_attribute_with_several_options
assert_equal accounts(:unknown), Account.find_by_credit_limit(50, :order => 'id DESC', :conditions => ['id != ?', 3])
end
def test_find_by_one_missing_attribute
assert_raises(NoMethodError) { Topic.find_by_undertitle("The First Topic!") }
end
def test_find_by_invalid_method_syntax
assert_raises(NoMethodError) { Topic.fail_to_find_by_title("The First Topic") }
assert_raises(NoMethodError) { Topic.find_by_title?("The First Topic") }
assert_raises(NoMethodError) { Topic.fail_to_find_or_create_by_title("Nonexistent Title") }
assert_raises(NoMethodError) { Topic.find_or_create_by_title?("Nonexistent Title") }
end
def test_find_by_two_attributes
assert_equal topics(:first), Topic.find_by_title_and_author_name("The First Topic", "David")
assert_nil Topic.find_by_title_and_author_name("The First Topic", "Mary")
end
def test_find_all_by_one_attribute
topics = Topic.find_all_by_content("Have a nice day")
assert_equal 2, topics.size
assert topics.include?(topics(:first))
assert_equal [], Topic.find_all_by_title("The First Topic!!")
end
def test_find_all_by_one_attribute_that_is_an_aggregate
balance = customers(:david).balance
assert_kind_of Money, balance
found_customers = Customer.find_all_by_balance(balance)
assert_equal 1, found_customers.size
assert_equal customers(:david), found_customers.first
end
def test_find_all_by_two_attributes_that_are_both_aggregates
balance = customers(:david).balance
address = customers(:david).address
assert_kind_of Money, balance
assert_kind_of Address, address
found_customers = Customer.find_all_by_balance_and_address(balance, address)
assert_equal 1, found_customers.size
assert_equal customers(:david), found_customers.first
end
def test_find_all_by_two_attributes_with_one_being_an_aggregate
balance = customers(:david).balance
assert_kind_of Money, balance
found_customers = Customer.find_all_by_balance_and_name(balance, customers(:david).name)
assert_equal 1, found_customers.size
assert_equal customers(:david), found_customers.first
end
def test_find_all_by_one_attribute_with_options
topics = Topic.find_all_by_content("Have a nice day", :order => "id DESC")
assert topics(:first), topics.last
topics = Topic.find_all_by_content("Have a nice day", :order => "id")
assert topics(:first), topics.first
end
def test_find_all_by_array_attribute
assert_equal 2, Topic.find_all_by_title(["The First Topic", "The Second Topic of the day"]).size
end
def test_find_all_by_boolean_attribute
topics = Topic.find_all_by_approved(false)
assert_equal 1, topics.size
assert topics.include?(topics(:first))
topics = Topic.find_all_by_approved(true)
assert_equal 3, topics.size
assert topics.include?(topics(:second))
end
def test_find_by_nil_attribute
topic = Topic.find_by_last_read nil
assert_not_nil topic
assert_nil topic.last_read
end
def test_find_all_by_nil_attribute
topics = Topic.find_all_by_last_read nil
assert_equal 3, topics.size
assert topics.collect(&:last_read).all?(&:nil?)
end
def test_find_by_nil_and_not_nil_attributes
topic = Topic.find_by_last_read_and_author_name nil, "Mary"
assert_equal "Mary", topic.author_name
end
def test_find_all_by_nil_and_not_nil_attributes
topics = Topic.find_all_by_last_read_and_author_name nil, "Mary"
assert_equal 1, topics.size
assert_equal "Mary", topics[0].author_name
end
def test_find_or_create_from_one_attribute
number_of_companies = Company.count
sig38 = Company.find_or_create_by_name("38signals")
assert_equal number_of_companies + 1, Company.count
assert_equal sig38, Company.find_or_create_by_name("38signals")
assert !sig38.new_record?
end
def test_find_or_create_from_two_attributes
number_of_topics = Topic.count
another = Topic.find_or_create_by_title_and_author_name("Another topic","John")
assert_equal number_of_topics + 1, Topic.count
assert_equal another, Topic.find_or_create_by_title_and_author_name("Another topic", "John")
assert !another.new_record?
end
def test_find_or_create_from_two_attributes_with_one_being_an_aggregate
number_of_customers = Customer.count
created_customer = Customer.find_or_create_by_balance_and_name(Money.new(123), "Elizabeth")
assert_equal number_of_customers + 1, Customer.count
assert_equal created_customer, Customer.find_or_create_by_balance(Money.new(123), "Elizabeth")
assert !created_customer.new_record?
end
def test_find_or_create_from_one_attribute_and_hash
number_of_companies = Company.count
sig38 = Company.find_or_create_by_name({:name => "38signals", :firm_id => 17, :client_of => 23})
assert_equal number_of_companies + 1, Company.count
assert_equal sig38, Company.find_or_create_by_name({:name => "38signals", :firm_id => 17, :client_of => 23})
assert !sig38.new_record?
assert_equal "38signals", sig38.name
assert_equal 17, sig38.firm_id
assert_equal 23, sig38.client_of
end
def test_find_or_create_from_one_aggregate_attribute
number_of_customers = Customer.count
created_customer = Customer.find_or_create_by_balance(Money.new(123))
assert_equal number_of_customers + 1, Customer.count
assert_equal created_customer, Customer.find_or_create_by_balance(Money.new(123))
assert !created_customer.new_record?
end
def test_find_or_create_from_one_aggregate_attribute_and_hash
number_of_customers = Customer.count
balance = Money.new(123)
name = "Elizabeth"
created_customer = Customer.find_or_create_by_balance({:balance => balance, :name => name})
assert_equal number_of_customers + 1, Customer.count
assert_equal created_customer, Customer.find_or_create_by_balance({:balance => balance, :name => name})
assert !created_customer.new_record?
assert_equal balance, created_customer.balance
assert_equal name, created_customer.name
end
def test_find_or_initialize_from_one_attribute
sig38 = Company.find_or_initialize_by_name("38signals")
assert_equal "38signals", sig38.name
assert sig38.new_record?
end
def test_find_or_initialize_from_one_aggregate_attribute
new_customer = Customer.find_or_initialize_by_balance(Money.new(123))
assert_equal 123, new_customer.balance.amount
assert new_customer.new_record?
end
def test_find_or_initialize_from_one_attribute_should_not_set_attribute_even_when_protected
c = Company.find_or_initialize_by_name({:name => "Fortune 1000", :rating => 1000})
assert_equal "Fortune 1000", c.name
assert_not_equal 1000, c.rating
assert c.valid?
assert c.new_record?
end
def test_find_or_create_from_one_attribute_should_set_not_attribute_even_when_protected
c = Company.find_or_create_by_name({:name => "Fortune 1000", :rating => 1000})
assert_equal "Fortune 1000", c.name
assert_not_equal 1000, c.rating
assert c.valid?
assert !c.new_record?
end
def test_find_or_initialize_from_one_attribute_should_set_attribute_even_when_protected
c = Company.find_or_initialize_by_name_and_rating("Fortune 1000", 1000)
assert_equal "Fortune 1000", c.name
assert_equal 1000, c.rating
assert c.valid?
assert c.new_record?
end
def test_find_or_create_from_one_attribute_should_set_attribute_even_when_protected
c = Company.find_or_create_by_name_and_rating("Fortune 1000", 1000)
assert_equal "Fortune 1000", c.name
assert_equal 1000, c.rating
assert c.valid?
assert !c.new_record?
end
def test_find_or_initialize_should_set_protected_attributes_if_given_as_block
c = Company.find_or_initialize_by_name(:name => "Fortune 1000") { |f| f.rating = 1000 }
assert_equal "Fortune 1000", c.name
assert_equal 1000.to_f, c.rating.to_f
assert c.valid?
assert c.new_record?
end
def test_find_or_create_should_set_protected_attributes_if_given_as_block
c = Company.find_or_create_by_name(:name => "Fortune 1000") { |f| f.rating = 1000 }
assert_equal "Fortune 1000", c.name
assert_equal 1000.to_f, c.rating.to_f
assert c.valid?
assert !c.new_record?
end
def test_dynamic_find_or_initialize_from_one_attribute_caches_method
class << Company; self; end.send(:remove_method, :find_or_initialize_by_name) if Company.public_methods.any? { |m| m.to_s == 'find_or_initialize_by_name' }
assert !Company.public_methods.any? { |m| m.to_s == 'find_or_initialize_by_name' }
sig38 = Company.find_or_initialize_by_name("38signals")
assert Company.public_methods.any? { |m| m.to_s == 'find_or_initialize_by_name' }
end
def test_find_or_initialize_from_two_attributes
another = Topic.find_or_initialize_by_title_and_author_name("Another topic","John")
assert_equal "Another topic", another.title
assert_equal "John", another.author_name
assert another.new_record?
end
def test_find_or_initialize_from_one_aggregate_attribute_and_one_not
new_customer = Customer.find_or_initialize_by_balance_and_name(Money.new(123), "Elizabeth")
assert_equal 123, new_customer.balance.amount
assert_equal "Elizabeth", new_customer.name
assert new_customer.new_record?
end
def test_find_or_initialize_from_one_attribute_and_hash
sig38 = Company.find_or_initialize_by_name({:name => "38signals", :firm_id => 17, :client_of => 23})
assert_equal "38signals", sig38.name
assert_equal 17, sig38.firm_id
assert_equal 23, sig38.client_of
assert sig38.new_record?
end
def test_find_or_initialize_from_one_aggregate_attribute_and_hash
balance = Money.new(123)
name = "Elizabeth"
new_customer = Customer.find_or_initialize_by_balance({:balance => balance, :name => name})
assert_equal balance, new_customer.balance
assert_equal name, new_customer.name
assert new_customer.new_record?
end
def test_find_with_bad_sql
assert_raises(ActiveRecord::StatementInvalid) { Topic.find_by_sql "select 1 from badtable" }
end
def test_find_with_invalid_params
assert_raises(ArgumentError) { Topic.find :first, :join => "It should be `joins'" }
assert_raises(ArgumentError) { Topic.find :first, :conditions => '1 = 1', :join => "It should be `joins'" }
end
def test_dynamic_finder_with_invalid_params
assert_raises(ArgumentError) { Topic.find_by_title 'No Title', :join => "It should be `joins'" }
end
def test_find_all_with_limit
first_five_developers = Developer.find :all, :order => 'id ASC', :limit => 5
assert_equal 5, first_five_developers.length
assert_equal 'David', first_five_developers.first.name
assert_equal 'fixture_5', first_five_developers.last.name
no_developers = Developer.find :all, :order => 'id ASC', :limit => 0
assert_equal 0, no_developers.length
end
def test_find_all_with_limit_and_offset
first_three_developers = Developer.find :all, :order => 'id ASC', :limit => 3, :offset => 0
second_three_developers = Developer.find :all, :order => 'id ASC', :limit => 3, :offset => 3
last_two_developers = Developer.find :all, :order => 'id ASC', :limit => 2, :offset => 8
assert_equal 3, first_three_developers.length
assert_equal 3, second_three_developers.length
assert_equal 2, last_two_developers.length
assert_equal 'David', first_three_developers.first.name
assert_equal 'fixture_4', second_three_developers.first.name
assert_equal 'fixture_9', last_two_developers.first.name
end
def test_find_all_with_limit_and_offset_and_multiple_order_clauses
first_three_posts = Post.find :all, :order => 'author_id, id', :limit => 3, :offset => 0
second_three_posts = Post.find :all, :order => ' author_id,id ', :limit => 3, :offset => 3
last_posts = Post.find :all, :order => ' author_id, id ', :limit => 3, :offset => 6
assert_equal [[0,3],[1,1],[1,2]], first_three_posts.map { |p| [p.author_id, p.id] }
assert_equal [[1,4],[1,5],[1,6]], second_three_posts.map { |p| [p.author_id, p.id] }
assert_equal [[2,7]], last_posts.map { |p| [p.author_id, p.id] }
end
def test_find_all_with_join
developers_on_project_one = Developer.find(
:all,
:joins => 'LEFT JOIN developers_projects ON developers.id = developers_projects.developer_id',
:conditions => 'project_id=1'
)
assert_equal 3, developers_on_project_one.length
developer_names = developers_on_project_one.map { |d| d.name }
assert developer_names.include?('David')
assert developer_names.include?('Jamis')
end
def test_joins_dont_clobber_id
first = Firm.find(
:first,
:joins => 'INNER JOIN companies AS clients ON clients.firm_id = companies.id',
:conditions => 'companies.id = 1'
)
assert_equal 1, first.id
end
def test_find_by_id_with_conditions_with_or
assert_nothing_raised do
Post.find([1,2,3],
:conditions => "posts.id <= 3 OR posts.#{QUOTED_TYPE} = 'Post'")
end
end
# http://dev.rubyonrails.org/ticket/6778
def test_find_ignores_previously_inserted_record
post = Post.create!(:title => 'test', :body => 'it out')
assert_equal [], Post.find_all_by_id(nil)
end
def test_find_by_empty_ids
assert_equal [], Post.find([])
end
def test_find_by_empty_in_condition
assert_equal [], Post.find(:all, :conditions => ['id in (?)', []])
end
def test_find_by_records
p1, p2 = Post.find(:all, :limit => 2, :order => 'id asc')
assert_equal [p1, p2], Post.find(:all, :conditions => ['id in (?)', [p1, p2]], :order => 'id asc')
assert_equal [p1, p2], Post.find(:all, :conditions => ['id in (?)', [p1, p2.id]], :order => 'id asc')
end
def test_select_value
assert_equal "37signals", Company.connection.select_value("SELECT name FROM companies WHERE id = 1")
assert_nil Company.connection.select_value("SELECT name FROM companies WHERE id = -1")
# make sure we didn't break count...
assert_equal 0, Company.count_by_sql("SELECT COUNT(*) FROM companies WHERE name = 'Halliburton'")
assert_equal 1, Company.count_by_sql("SELECT COUNT(*) FROM companies WHERE name = '37signals'")
end
def test_select_values
assert_equal ["1","2","3","4","5","6","7","8","9"], Company.connection.select_values("SELECT id FROM companies ORDER BY id").map! { |i| i.to_s }
assert_equal ["37signals","Summit","Microsoft", "Flamboyant Software", "Ex Nihilo", "RailsCore", "Leetsoft", "Jadedpixel", "Odegy"], Company.connection.select_values("SELECT name FROM companies ORDER BY id")
end
def test_select_rows
assert_equal(
[["1", nil, nil, "37signals"],
["2", "1", "2", "Summit"],
["3", "1", "1", "Microsoft"]],
Company.connection.select_rows("SELECT id, firm_id, client_of, name FROM companies WHERE id IN (1,2,3) ORDER BY id").map! {|i| i.map! {|j| j.to_s unless j.nil?}})
assert_equal [["1", "37signals"], ["2", "Summit"], ["3", "Microsoft"]],
Company.connection.select_rows("SELECT id, name FROM companies WHERE id IN (1,2,3) ORDER BY id").map! {|i| i.map! {|j| j.to_s unless j.nil?}}
end
def test_find_with_order_on_included_associations_with_construct_finder_sql_for_association_limiting_and_is_distinct
assert_equal 2, Post.find(:all, :include => { :authors => :author_address }, :order => ' author_addresses.id DESC ', :limit => 2).size
assert_equal 3, Post.find(:all, :include => { :author => :author_address, :authors => :author_address},
:order => ' author_addresses_authors.id DESC ', :limit => 3).size
end
def test_with_limiting_with_custom_select
posts = Post.find(:all, :include => :author, :select => ' posts.*, authors.id as "author_id"', :limit => 3, :order => 'posts.id')
assert_equal 3, posts.size
assert_equal [0, 1, 1], posts.map(&:author_id).sort
end
protected
def bind(statement, *vars)
if vars.first.is_a?(Hash)
ActiveRecord::Base.send(:replace_named_bind_variables, statement, vars.first)
else
ActiveRecord::Base.send(:replace_bind_variables, statement, vars)
end
end
end

View file

@ -1,626 +0,0 @@
require "cases/helper"
require 'models/post'
require 'models/binary'
require 'models/topic'
require 'models/computer'
require 'models/developer'
require 'models/company'
require 'models/task'
require 'models/reply'
require 'models/joke'
require 'models/course'
require 'models/category'
require 'models/parrot'
require 'models/pirate'
require 'models/treasure'
require 'models/matey'
require 'models/ship'
class FixturesTest < ActiveRecord::TestCase
self.use_instantiated_fixtures = true
self.use_transactional_fixtures = false
fixtures :topics, :developers, :accounts, :tasks, :categories, :funny_jokes, :binaries
FIXTURES = %w( accounts binaries companies customers
developers developers_projects entrants
movies projects subscribers topics tasks )
MATCH_ATTRIBUTE_NAME = /[a-zA-Z][-_\w]*/
def test_clean_fixtures
FIXTURES.each do |name|
fixtures = nil
assert_nothing_raised { fixtures = create_fixtures(name) }
assert_kind_of(Fixtures, fixtures)
fixtures.each { |name, fixture|
fixture.each { |key, value|
assert_match(MATCH_ATTRIBUTE_NAME, key)
}
}
end
end
def test_multiple_clean_fixtures
fixtures_array = nil
assert_nothing_raised { fixtures_array = create_fixtures(*FIXTURES) }
assert_kind_of(Array, fixtures_array)
fixtures_array.each { |fixtures| assert_kind_of(Fixtures, fixtures) }
end
def test_attributes
topics = create_fixtures("topics")
assert_equal("The First Topic", topics["first"]["title"])
assert_nil(topics["second"]["author_email_address"])
end
def test_inserts
topics = create_fixtures("topics")
first_row = ActiveRecord::Base.connection.select_one("SELECT * FROM topics WHERE author_name = 'David'")
assert_equal("The First Topic", first_row["title"])
second_row = ActiveRecord::Base.connection.select_one("SELECT * FROM topics WHERE author_name = 'Mary'")
assert_nil(second_row["author_email_address"])
end
if ActiveRecord::Base.connection.supports_migrations?
def test_inserts_with_pre_and_suffix
# Reset cache to make finds on the new table work
Fixtures.reset_cache
ActiveRecord::Base.connection.create_table :prefix_topics_suffix do |t|
t.column :title, :string
t.column :author_name, :string
t.column :author_email_address, :string
t.column :written_on, :datetime
t.column :bonus_time, :time
t.column :last_read, :date
t.column :content, :string
t.column :approved, :boolean, :default => true
t.column :replies_count, :integer, :default => 0
t.column :parent_id, :integer
t.column :type, :string, :limit => 50
end
# Store existing prefix/suffix
old_prefix = ActiveRecord::Base.table_name_prefix
old_suffix = ActiveRecord::Base.table_name_suffix
# Set a prefix/suffix we can test against
ActiveRecord::Base.table_name_prefix = 'prefix_'
ActiveRecord::Base.table_name_suffix = '_suffix'
topics = create_fixtures("topics")
first_row = ActiveRecord::Base.connection.select_one("SELECT * FROM prefix_topics_suffix WHERE author_name = 'David'")
assert_equal("The First Topic", first_row["title"])
second_row = ActiveRecord::Base.connection.select_one("SELECT * FROM prefix_topics_suffix WHERE author_name = 'Mary'")
assert_nil(second_row["author_email_address"])
# This checks for a caching problem which causes a bug in the fixtures
# class-level configuration helper.
assert_not_nil topics, "Fixture data inserted, but fixture objects not returned from create"
ensure
# Restore prefix/suffix to its previous values
ActiveRecord::Base.table_name_prefix = old_prefix
ActiveRecord::Base.table_name_suffix = old_suffix
ActiveRecord::Base.connection.drop_table :prefix_topics_suffix rescue nil
end
end
def test_insert_with_datetime
topics = create_fixtures("tasks")
first = Task.find(1)
assert first
end
def test_logger_level_invariant
level = ActiveRecord::Base.logger.level
create_fixtures('topics')
assert_equal level, ActiveRecord::Base.logger.level
end
def test_instantiation
topics = create_fixtures("topics")
assert_kind_of Topic, topics["first"].find
end
def test_complete_instantiation
assert_equal 4, @topics.size
assert_equal "The First Topic", @first.title
end
def test_fixtures_from_root_yml_with_instantiation
# assert_equal 2, @accounts.size
assert_equal 50, @unknown.credit_limit
end
def test_erb_in_fixtures
assert_equal 11, @developers.size
assert_equal "fixture_5", @dev_5.name
end
def test_empty_yaml_fixture
assert_not_nil Fixtures.new( Account.connection, "accounts", 'Account', FIXTURES_ROOT + "/naked/yml/accounts")
end
def test_empty_yaml_fixture_with_a_comment_in_it
assert_not_nil Fixtures.new( Account.connection, "companies", 'Company', FIXTURES_ROOT + "/naked/yml/companies")
end
def test_dirty_dirty_yaml_file
assert_raises(Fixture::FormatError) do
Fixtures.new( Account.connection, "courses", 'Course', FIXTURES_ROOT + "/naked/yml/courses")
end
end
def test_empty_csv_fixtures
assert_not_nil Fixtures.new( Account.connection, "accounts", 'Account', FIXTURES_ROOT + "/naked/csv/accounts")
end
def test_omap_fixtures
assert_nothing_raised do
fixtures = Fixtures.new(Account.connection, 'categories', 'Category', FIXTURES_ROOT + "/categories_ordered")
i = 0
fixtures.each do |name, fixture|
assert_equal "fixture_no_#{i}", name
assert_equal "Category #{i}", fixture['name']
i += 1
end
end
end
def test_yml_file_in_subdirectory
assert_equal(categories(:sub_special_1).name, "A special category in a subdir file")
assert_equal(categories(:sub_special_1).class, SpecialCategory)
end
def test_subsubdir_file_with_arbitrary_name
assert_equal(categories(:sub_special_3).name, "A special category in an arbitrarily named subsubdir file")
assert_equal(categories(:sub_special_3).class, SpecialCategory)
end
def test_binary_in_fixtures
assert_equal 1, @binaries.size
data = File.read(ASSETS_ROOT + "/flowers.jpg")
data.force_encoding('ASCII-8BIT') if data.respond_to?(:force_encoding)
data.freeze
assert_equal data, @flowers.data
end
end
if Account.connection.respond_to?(:reset_pk_sequence!)
class FixturesResetPkSequenceTest < ActiveRecord::TestCase
fixtures :accounts
fixtures :companies
def setup
@instances = [Account.new(:credit_limit => 50), Company.new(:name => 'RoR Consulting')]
Fixtures.reset_cache # make sure tables get reinitialized
end
def test_resets_to_min_pk_with_specified_pk_and_sequence
@instances.each do |instance|
model = instance.class
model.delete_all
model.connection.reset_pk_sequence!(model.table_name, model.primary_key, model.sequence_name)
instance.save!
assert_equal 1, instance.id, "Sequence reset for #{model.table_name} failed."
end
end
def test_resets_to_min_pk_with_default_pk_and_sequence
@instances.each do |instance|
model = instance.class
model.delete_all
model.connection.reset_pk_sequence!(model.table_name)
instance.save!
assert_equal 1, instance.id, "Sequence reset for #{model.table_name} failed."
end
end
def test_create_fixtures_resets_sequences_when_not_cached
@instances.each do |instance|
max_id = create_fixtures(instance.class.table_name).inject(0) do |max_id, (name, fixture)|
fixture_id = fixture['id'].to_i
fixture_id > max_id ? fixture_id : max_id
end
# Clone the last fixture to check that it gets the next greatest id.
instance.save!
assert_equal max_id + 1, instance.id, "Sequence reset for #{instance.class.table_name} failed."
end
end
end
end
class FixturesWithoutInstantiationTest < ActiveRecord::TestCase
self.use_instantiated_fixtures = false
fixtures :topics, :developers, :accounts
def test_without_complete_instantiation
assert_nil @first
assert_nil @topics
assert_nil @developers
assert_nil @accounts
end
def test_fixtures_from_root_yml_without_instantiation
assert_nil @unknown
end
def test_accessor_methods
assert_equal "The First Topic", topics(:first).title
assert_equal "Jamis", developers(:jamis).name
assert_equal 50, accounts(:signals37).credit_limit
end
def test_accessor_methods_with_multiple_args
assert_equal 2, topics(:first, :second).size
assert_raise(StandardError) { topics([:first, :second]) }
end
uses_mocha 'reloading_fixtures_through_accessor_methods' do
def test_reloading_fixtures_through_accessor_methods
assert_equal "The First Topic", topics(:first).title
@loaded_fixtures['topics']['first'].expects(:find).returns(stub(:title => "Fresh Topic!"))
assert_equal "Fresh Topic!", topics(:first, true).title
end
end
end
class FixturesWithoutInstanceInstantiationTest < ActiveRecord::TestCase
self.use_instantiated_fixtures = true
self.use_instantiated_fixtures = :no_instances
fixtures :topics, :developers, :accounts
def test_without_instance_instantiation
assert_nil @first
assert_not_nil @topics
assert_not_nil @developers
assert_not_nil @accounts
end
end
class TransactionalFixturesTest < ActiveRecord::TestCase
self.use_instantiated_fixtures = true
self.use_transactional_fixtures = true
fixtures :topics
def test_destroy
assert_not_nil @first
@first.destroy
end
def test_destroy_just_kidding
assert_not_nil @first
end
end
class MultipleFixturesTest < ActiveRecord::TestCase
fixtures :topics
fixtures :developers, :accounts
def test_fixture_table_names
assert_equal %w(topics developers accounts), fixture_table_names
end
end
class SetupTest < ActiveRecord::TestCase
# fixtures :topics
def setup
@first = true
end
def test_nothing
end
end
class SetupSubclassTest < SetupTest
def setup
super
@second = true
end
def test_subclassing_should_preserve_setups
assert @first
assert @second
end
end
class OverlappingFixturesTest < ActiveRecord::TestCase
fixtures :topics, :developers
fixtures :developers, :accounts
def test_fixture_table_names
assert_equal %w(topics developers accounts), fixture_table_names
end
end
class ForeignKeyFixturesTest < ActiveRecord::TestCase
fixtures :fk_test_has_pk, :fk_test_has_fk
# if foreign keys are implemented and fixtures
# are not deleted in reverse order then this test
# case will raise StatementInvalid
def test_number1
assert true
end
def test_number2
assert true
end
end
class CheckSetTableNameFixturesTest < ActiveRecord::TestCase
set_fixture_class :funny_jokes => 'Joke'
fixtures :funny_jokes
# Set to false to blow away fixtures cache and ensure our fixtures are loaded
# and thus takes into account our set_fixture_class
self.use_transactional_fixtures = false
def test_table_method
assert_kind_of Joke, funny_jokes(:a_joke)
end
end
class CustomConnectionFixturesTest < ActiveRecord::TestCase
set_fixture_class :courses => Course
fixtures :courses
# Set to false to blow away fixtures cache and ensure our fixtures are loaded
# and thus takes into account our set_fixture_class
self.use_transactional_fixtures = false
def test_connection
assert_kind_of Course, courses(:ruby)
assert_equal Course.connection, courses(:ruby).connection
end
end
class InvalidTableNameFixturesTest < ActiveRecord::TestCase
fixtures :funny_jokes
# Set to false to blow away fixtures cache and ensure our fixtures are loaded
# and thus takes into account our lack of set_fixture_class
self.use_transactional_fixtures = false
def test_raises_error
assert_raises FixtureClassNotFound do
funny_jokes(:a_joke)
end
end
end
class CheckEscapedYamlFixturesTest < ActiveRecord::TestCase
set_fixture_class :funny_jokes => 'Joke'
fixtures :funny_jokes
# Set to false to blow away fixtures cache and ensure our fixtures are loaded
# and thus takes into account our set_fixture_class
self.use_transactional_fixtures = false
def test_proper_escaped_fixture
assert_equal "The \\n Aristocrats\nAte the candy\n", funny_jokes(:another_joke).name
end
end
class DevelopersProject; end
class ManyToManyFixturesWithClassDefined < ActiveRecord::TestCase
fixtures :developers_projects
def test_this_should_run_cleanly
assert true
end
end
class FixturesBrokenRollbackTest < ActiveRecord::TestCase
def blank_setup; end
alias_method :ar_setup_fixtures, :setup_fixtures
alias_method :setup_fixtures, :blank_setup
alias_method :setup, :blank_setup
def blank_teardown; end
alias_method :ar_teardown_fixtures, :teardown_fixtures
alias_method :teardown_fixtures, :blank_teardown
alias_method :teardown, :blank_teardown
def test_no_rollback_in_teardown_unless_transaction_active
assert_equal 0, Thread.current['open_transactions']
assert_raise(RuntimeError) { ar_setup_fixtures }
assert_equal 0, Thread.current['open_transactions']
assert_nothing_raised { ar_teardown_fixtures }
assert_equal 0, Thread.current['open_transactions']
end
private
def load_fixtures
raise 'argh'
end
end
class LoadAllFixturesTest < ActiveRecord::TestCase
self.fixture_path = FIXTURES_ROOT + "/all"
fixtures :all
def test_all_there
assert_equal %w(developers people tasks), fixture_table_names.sort
end
end
class FasterFixturesTest < ActiveRecord::TestCase
fixtures :categories, :authors
def load_extra_fixture(name)
fixture = create_fixtures(name)
assert fixture.is_a?(Fixtures)
@loaded_fixtures[fixture.table_name] = fixture
end
def test_cache
assert Fixtures.fixture_is_cached?(ActiveRecord::Base.connection, 'categories')
assert Fixtures.fixture_is_cached?(ActiveRecord::Base.connection, 'authors')
assert_no_queries do
create_fixtures('categories')
create_fixtures('authors')
end
load_extra_fixture('posts')
assert Fixtures.fixture_is_cached?(ActiveRecord::Base.connection, 'posts')
self.class.setup_fixture_accessors('posts')
assert_equal 'Welcome to the weblog', posts(:welcome).title
end
end
class FoxyFixturesTest < ActiveRecord::TestCase
fixtures :parrots, :parrots_pirates, :pirates, :treasures, :mateys, :ships, :computers, :developers
def test_identifies_strings
assert_equal(Fixtures.identify("foo"), Fixtures.identify("foo"))
assert_not_equal(Fixtures.identify("foo"), Fixtures.identify("FOO"))
end
def test_identifies_symbols
assert_equal(Fixtures.identify(:foo), Fixtures.identify(:foo))
end
TIMESTAMP_COLUMNS = %w(created_at created_on updated_at updated_on)
def test_populates_timestamp_columns
TIMESTAMP_COLUMNS.each do |property|
assert_not_nil(parrots(:george).send(property), "should set #{property}")
end
end
def test_does_not_populate_timestamp_columns_if_model_has_set_record_timestamps_to_false
TIMESTAMP_COLUMNS.each do |property|
assert_nil(ships(:black_pearl).send(property), "should not set #{property}")
end
end
def test_populates_all_columns_with_the_same_time
last = nil
TIMESTAMP_COLUMNS.each do |property|
current = parrots(:george).send(property)
last ||= current
assert_equal(last, current)
last = current
end
end
def test_only_populates_columns_that_exist
assert_not_nil(pirates(:blackbeard).created_on)
assert_not_nil(pirates(:blackbeard).updated_on)
end
def test_preserves_existing_fixture_data
assert_equal(2.weeks.ago.to_date, pirates(:redbeard).created_on.to_date)
assert_equal(2.weeks.ago.to_date, pirates(:redbeard).updated_on.to_date)
end
def test_generates_unique_ids
assert_not_nil(parrots(:george).id)
assert_not_equal(parrots(:george).id, parrots(:louis).id)
end
def test_automatically_sets_primary_key
assert_not_nil(ships(:black_pearl))
end
def test_preserves_existing_primary_key
assert_equal(2, ships(:interceptor).id)
end
def test_resolves_belongs_to_symbols
assert_equal(parrots(:george), pirates(:blackbeard).parrot)
end
def test_ignores_belongs_to_symbols_if_association_and_foreign_key_are_named_the_same
assert_equal(developers(:david), computers(:workstation).developer)
end
def test_supports_join_tables
assert(pirates(:blackbeard).parrots.include?(parrots(:george)))
assert(pirates(:blackbeard).parrots.include?(parrots(:louis)))
assert(parrots(:george).pirates.include?(pirates(:blackbeard)))
end
def test_supports_inline_habtm
assert(parrots(:george).treasures.include?(treasures(:diamond)))
assert(parrots(:george).treasures.include?(treasures(:sapphire)))
assert(!parrots(:george).treasures.include?(treasures(:ruby)))
end
def test_supports_inline_habtm_with_specified_id
assert(parrots(:polly).treasures.include?(treasures(:ruby)))
assert(parrots(:polly).treasures.include?(treasures(:sapphire)))
assert(!parrots(:polly).treasures.include?(treasures(:diamond)))
end
def test_supports_yaml_arrays
assert(parrots(:louis).treasures.include?(treasures(:diamond)))
assert(parrots(:louis).treasures.include?(treasures(:sapphire)))
end
def test_strips_DEFAULTS_key
assert_raise(StandardError) { parrots(:DEFAULTS) }
# this lets us do YAML defaults and not have an extra fixture entry
%w(sapphire ruby).each { |t| assert(parrots(:davey).treasures.include?(treasures(t))) }
end
def test_supports_label_interpolation
assert_equal("frederick", parrots(:frederick).name)
end
def test_supports_polymorphic_belongs_to
assert_equal(pirates(:redbeard), treasures(:sapphire).looter)
assert_equal(parrots(:louis), treasures(:ruby).looter)
end
def test_only_generates_a_pk_if_necessary
m = Matey.find(:first)
m.pirate = pirates(:blackbeard)
m.target = pirates(:redbeard)
end
def test_supports_sti
assert_kind_of DeadParrot, parrots(:polly)
assert_equal pirates(:blackbeard), parrots(:polly).killer
end
end
class ActiveSupportSubclassWithFixturesTest < ActiveRecord::TestCase
fixtures :parrots
# This seemingly useless assertion catches a bug that caused the fixtures
# setup code call nil[]
def test_foo
assert_equal parrots(:louis), Parrot.find_by_name("King Louis")
end
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::Base.logger.expects(:warn)
Test::Unit::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::Base.logger.expects(:warn).never
Test::Unit::TestCase.try_to_load_dependency(:works_out_fine)
end
end
end

Some files were not shown because too many files have changed in this diff Show more