mirror of
https://github.com/TracksApp/tracks.git
synced 2026-03-02 02:50:16 +01:00
unfreeze rails
This commit is contained in:
parent
bd2b410c7b
commit
fe5f962dcf
1493 changed files with 1 additions and 191145 deletions
5719
vendor/rails/activerecord/CHANGELOG
vendored
5719
vendor/rails/activerecord/CHANGELOG
vendored
File diff suppressed because it is too large
Load diff
20
vendor/rails/activerecord/MIT-LICENSE
vendored
20
vendor/rails/activerecord/MIT-LICENSE
vendored
|
|
@ -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.
|
||||
351
vendor/rails/activerecord/README
vendored
351
vendor/rails/activerecord/README
vendored
|
|
@ -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.
|
||||
36
vendor/rails/activerecord/RUNNING_UNIT_TESTS
vendored
36
vendor/rails/activerecord/RUNNING_UNIT_TESTS
vendored
|
|
@ -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
|
||||
|
||||
|
||||
|
||||
247
vendor/rails/activerecord/Rakefile
vendored
247
vendor/rails/activerecord/Rakefile
vendored
|
|
@ -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
|
||||
BIN
vendor/rails/activerecord/examples/associations.png
vendored
BIN
vendor/rails/activerecord/examples/associations.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 40 KiB |
30
vendor/rails/activerecord/install.rb
vendored
30
vendor/rails/activerecord/install.rb
vendored
|
|
@ -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
|
||||
}
|
||||
82
vendor/rails/activerecord/lib/active_record.rb
vendored
82
vendor/rails/activerecord/lib/active_record.rb
vendored
|
|
@ -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'
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
2726
vendor/rails/activerecord/lib/active_record/base.rb
vendored
2726
vendor/rails/activerecord/lib/active_record/base.rb
vendored
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
158
vendor/rails/activerecord/lib/active_record/dirty.rb
vendored
158
vendor/rails/activerecord/lib/active_record/dirty.rb
vendored
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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'
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
|
@ -1,9 +0,0 @@
|
|||
module ActiveRecord
|
||||
module VERSION #:nodoc:
|
||||
MAJOR = 2
|
||||
MINOR = 1
|
||||
TINY = 0
|
||||
|
||||
STRING = [MAJOR, MINOR, TINY].join('.')
|
||||
end
|
||||
end
|
||||
|
|
@ -1 +0,0 @@
|
|||
require 'active_record'
|
||||
|
|
@ -1 +0,0 @@
|
|||
# Logfile created on Wed Oct 31 16:05:13 +0000 2007 by logger.rb/1.5.2.9
|
||||
BIN
vendor/rails/activerecord/test/assets/flowers.jpg
vendored
BIN
vendor/rails/activerecord/test/assets/flowers.jpg
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 5.7 KiB |
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
127
vendor/rails/activerecord/test/cases/adapter_test.rb
vendored
127
vendor/rails/activerecord/test/cases/adapter_test.rb
vendored
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
1974
vendor/rails/activerecord/test/cases/base_test.rb
vendored
1974
vendor/rails/activerecord/test/cases/base_test.rb
vendored
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
163
vendor/rails/activerecord/test/cases/dirty_test.rb
vendored
163
vendor/rails/activerecord/test/cases/dirty_test.rb
vendored
|
|
@ -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
|
||||
|
|
@ -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
|
||||
883
vendor/rails/activerecord/test/cases/finder_test.rb
vendored
883
vendor/rails/activerecord/test/cases/finder_test.rb
vendored
|
|
@ -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
|
||||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue