mirror of
https://github.com/TracksApp/tracks.git
synced 2026-02-22 07:04:09 +01:00
unfreeze rails 2.3.9
This commit is contained in:
parent
6443adac78
commit
dea6dbe4da
1916 changed files with 0 additions and 240923 deletions
5898
vendor/rails/activerecord/CHANGELOG
vendored
5898
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-2010 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
|
||||
|
||||
|
||||
|
||||
270
vendor/rails/activerecord/Rakefile
vendored
270
vendor/rails/activerecord/Rakefile
vendored
|
|
@ -1,270 +0,0 @@
|
|||
require 'rubygems'
|
||||
require 'rake'
|
||||
require 'rake/testtask'
|
||||
require 'rake/rdoctask'
|
||||
require 'rake/packagetask'
|
||||
require 'rake/gempackagetask'
|
||||
|
||||
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|~$/)
|
||||
|
||||
def run_without_aborting(*tasks)
|
||||
errors = []
|
||||
|
||||
tasks.each do |task|
|
||||
begin
|
||||
Rake::Task[task].invoke
|
||||
rescue Exception
|
||||
errors << task
|
||||
end
|
||||
end
|
||||
|
||||
abort "Errors running #{errors.join(', ')}" if errors.any?
|
||||
end
|
||||
|
||||
desc 'Run mysql, sqlite, and postgresql tests by default'
|
||||
task :default => :test
|
||||
|
||||
desc 'Run mysql, sqlite, and postgresql tests'
|
||||
task :test do
|
||||
tasks = defined?(JRUBY_VERSION) ?
|
||||
%w(test_jdbcmysql test_jdbcsqlite3 test_jdbcpostgresql) :
|
||||
%w(test_mysql test_sqlite3 test_postgresql)
|
||||
run_without_aborting(*tasks)
|
||||
end
|
||||
|
||||
for adapter in %w( mysql postgresql sqlite sqlite3 firebird db2 oracle sybase openbase frontbase jdbcmysql jdbcpostgresql jdbcsqlite3 jdbcderby jdbch2 jdbchsqldb )
|
||||
Rake::TestTask.new("test_#{adapter}") { |t|
|
||||
if adapter =~ /jdbc/
|
||||
t.libs << "test" << "test/connections/jdbc_#{adapter}"
|
||||
else
|
||||
t.libs << "test" << "test/connections/native_#{adapter}"
|
||||
end
|
||||
adapter_short = adapter == 'db2' ? adapter : adapter[/^[a-z]+/]
|
||||
t.test_files=Dir.glob( "test/cases/**/*_test{,_#{adapter_short}}.rb" ).sort
|
||||
t.verbose = true
|
||||
}
|
||||
|
||||
namespace adapter do
|
||||
task :test => "test_#{adapter}"
|
||||
end
|
||||
end
|
||||
|
||||
namespace :mysql do
|
||||
desc 'Build the MySQL test databases'
|
||||
task :build_databases do
|
||||
%x( echo "create DATABASE activerecord_unittest DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_unicode_ci " | mysql --user=#{MYSQL_DB_USER})
|
||||
%x( echo "create DATABASE activerecord_unittest2 DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_unicode_ci " | mysql --user=#{MYSQL_DB_USER})
|
||||
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 -E UTF8 activerecord_unittest )
|
||||
%x( createdb -E UTF8 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'] ? "#{ENV['template']}.rb" : '../doc/template/horo'
|
||||
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.3.9' + 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
|
||||
require 'rake/contrib/sshpublisher'
|
||||
Rake::SshFilePublisher.new("gems.rubyonrails.org", "/u/sites/gems/gems", "pkg", "#{PKG_FILE_NAME}.gem").upload
|
||||
`ssh gems.rubyonrails.org '/u/sites/gems/gemupdate.sh'`
|
||||
end
|
||||
|
||||
desc "Publish the API documentation"
|
||||
task :pdoc => [:rdoc] do
|
||||
require 'rake/contrib/sshpublisher'
|
||||
Rake::SshDirPublisher.new("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
|
||||
|
|
@ -1 +0,0 @@
|
|||
performance.sql
|
||||
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 |
164
vendor/rails/activerecord/examples/performance.rb
vendored
164
vendor/rails/activerecord/examples/performance.rb
vendored
|
|
@ -1,164 +0,0 @@
|
|||
#!/usr/bin/env ruby -KU
|
||||
|
||||
TIMES = (ENV['N'] || 10000).to_i
|
||||
|
||||
require 'rubygems'
|
||||
gem 'addressable', '~>2.0'
|
||||
gem 'faker', '~>0.3.1'
|
||||
gem 'rbench', '~>0.2.3'
|
||||
require 'addressable/uri'
|
||||
require 'faker'
|
||||
require 'rbench'
|
||||
|
||||
__DIR__ = File.dirname(__FILE__)
|
||||
$:.unshift "#{__DIR__}/../lib"
|
||||
$:.unshift "#{__DIR__}/../../activesupport/lib"
|
||||
|
||||
require 'active_record'
|
||||
|
||||
conn = { :adapter => 'mysql',
|
||||
:database => 'activerecord_unittest',
|
||||
:username => 'rails', :password => '',
|
||||
:encoding => 'utf8' }
|
||||
|
||||
conn[:socket] = Pathname.glob(%w[
|
||||
/opt/local/var/run/mysql5/mysqld.sock
|
||||
/tmp/mysqld.sock
|
||||
/tmp/mysql.sock
|
||||
/var/mysql/mysql.sock
|
||||
/var/run/mysqld/mysqld.sock
|
||||
]).find { |path| path.socket? }
|
||||
|
||||
ActiveRecord::Base.establish_connection(conn)
|
||||
|
||||
class User < ActiveRecord::Base
|
||||
connection.create_table :users, :force => true do |t|
|
||||
t.string :name, :email
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
has_many :exhibits
|
||||
end
|
||||
|
||||
class Exhibit < ActiveRecord::Base
|
||||
connection.create_table :exhibits, :force => true do |t|
|
||||
t.belongs_to :user
|
||||
t.string :name
|
||||
t.text :notes
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
belongs_to :user
|
||||
|
||||
def look; attributes end
|
||||
def feel; look; user.name end
|
||||
|
||||
def self.look(exhibits) exhibits.each { |e| e.look } end
|
||||
def self.feel(exhibits) exhibits.each { |e| e.feel } end
|
||||
end
|
||||
|
||||
sqlfile = "#{__DIR__}/performance.sql"
|
||||
|
||||
if File.exists?(sqlfile)
|
||||
mysql_bin = %w[mysql mysql5].select { |bin| `which #{bin}`.length > 0 }
|
||||
`#{mysql_bin} -u #{conn[:username]} #{"-p#{conn[:password]}" unless conn[:password].blank?} #{conn[:database]} < #{sqlfile}`
|
||||
else
|
||||
puts 'Generating data...'
|
||||
|
||||
# pre-compute the insert statements and fake data compilation,
|
||||
# so the benchmarks below show the actual runtime for the execute
|
||||
# method, minus the setup steps
|
||||
|
||||
# Using the same paragraph for all exhibits because it is very slow
|
||||
# to generate unique paragraphs for all exhibits.
|
||||
notes = Faker::Lorem.paragraphs.join($/)
|
||||
today = Date.today
|
||||
|
||||
puts 'Inserting 10,000 users and exhibits...'
|
||||
10_000.times do
|
||||
user = User.create(
|
||||
:created_at => today,
|
||||
:name => Faker::Name.name,
|
||||
:email => Faker::Internet.email
|
||||
)
|
||||
|
||||
Exhibit.create(
|
||||
:created_at => today,
|
||||
:name => Faker::Company.name,
|
||||
:user => user,
|
||||
:notes => notes
|
||||
)
|
||||
end
|
||||
|
||||
mysqldump_bin = %w[mysqldump mysqldump5].select { |bin| `which #{bin}`.length > 0 }
|
||||
`#{mysqldump_bin} -u #{conn[:username]} #{"-p#{conn[:password]}" unless conn[:password].blank?} #{conn[:database]} exhibits users > #{sqlfile}`
|
||||
end
|
||||
|
||||
RBench.run(TIMES) do
|
||||
column :times
|
||||
column :ar
|
||||
|
||||
report 'Model#id', (TIMES * 100).ceil do
|
||||
ar_obj = Exhibit.find(1)
|
||||
|
||||
ar { ar_obj.id }
|
||||
end
|
||||
|
||||
report 'Model.new (instantiation)' do
|
||||
ar { Exhibit.new }
|
||||
end
|
||||
|
||||
report 'Model.new (setting attributes)' do
|
||||
attrs = { :name => 'sam' }
|
||||
ar { Exhibit.new(attrs) }
|
||||
end
|
||||
|
||||
report 'Model.first' do
|
||||
ar { Exhibit.first.look }
|
||||
end
|
||||
|
||||
report 'Model.all limit(100)', (TIMES / 10).ceil do
|
||||
ar { Exhibit.look Exhibit.all(:limit => 100) }
|
||||
end
|
||||
|
||||
report 'Model.all limit(100) with relationship', (TIMES / 10).ceil do
|
||||
ar { Exhibit.feel Exhibit.all(:limit => 100, :include => :user) }
|
||||
end
|
||||
|
||||
report 'Model.all limit(10,000)', (TIMES / 1000).ceil do
|
||||
ar { Exhibit.look Exhibit.all(:limit => 10000) }
|
||||
end
|
||||
|
||||
exhibit = {
|
||||
:name => Faker::Company.name,
|
||||
:notes => Faker::Lorem.paragraphs.join($/),
|
||||
:created_at => Date.today
|
||||
}
|
||||
|
||||
report 'Model.create' do
|
||||
ar { Exhibit.create(exhibit) }
|
||||
end
|
||||
|
||||
report 'Resource#attributes=' do
|
||||
attrs_first = { :name => 'sam' }
|
||||
attrs_second = { :name => 'tom' }
|
||||
ar { exhibit = Exhibit.new(attrs_first); exhibit.attributes = attrs_second }
|
||||
end
|
||||
|
||||
report 'Resource#update' do
|
||||
ar { Exhibit.first.update_attributes(:name => 'bob') }
|
||||
end
|
||||
|
||||
report 'Resource#destroy' do
|
||||
ar { Exhibit.first.destroy }
|
||||
end
|
||||
|
||||
report 'Model.transaction' do
|
||||
ar { Exhibit.transaction { Exhibit.new } }
|
||||
end
|
||||
|
||||
summary 'Total'
|
||||
end
|
||||
|
||||
ActiveRecord::Migration.drop_table "exhibits"
|
||||
ActiveRecord::Migration.drop_table "users"
|
||||
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
|
||||
}
|
||||
83
vendor/rails/activerecord/lib/active_record.rb
vendored
83
vendor/rails/activerecord/lib/active_record.rb
vendored
|
|
@ -1,83 +0,0 @@
|
|||
#--
|
||||
# Copyright (c) 2004-2010 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.
|
||||
#++
|
||||
|
||||
begin
|
||||
require 'active_support'
|
||||
rescue LoadError
|
||||
activesupport_path = "#{File.dirname(__FILE__)}/../../activesupport/lib"
|
||||
if File.directory?(activesupport_path)
|
||||
$:.unshift activesupport_path
|
||||
require 'active_support'
|
||||
end
|
||||
end
|
||||
|
||||
module ActiveRecord
|
||||
# TODO: Review explicit loads to see if they will automatically be handled by the initilizer.
|
||||
def self.load_all!
|
||||
[Base, DynamicFinderMatch, ConnectionAdapters::AbstractAdapter]
|
||||
end
|
||||
|
||||
autoload :VERSION, 'active_record/version'
|
||||
|
||||
autoload :ActiveRecordError, 'active_record/base'
|
||||
autoload :ConnectionNotEstablished, 'active_record/base'
|
||||
|
||||
autoload :Aggregations, 'active_record/aggregations'
|
||||
autoload :AssociationPreload, 'active_record/association_preload'
|
||||
autoload :Associations, 'active_record/associations'
|
||||
autoload :AttributeMethods, 'active_record/attribute_methods'
|
||||
autoload :AutosaveAssociation, 'active_record/autosave_association'
|
||||
autoload :Base, 'active_record/base'
|
||||
autoload :Batches, 'active_record/batches'
|
||||
autoload :Calculations, 'active_record/calculations'
|
||||
autoload :Callbacks, 'active_record/callbacks'
|
||||
autoload :Dirty, 'active_record/dirty'
|
||||
autoload :DynamicFinderMatch, 'active_record/dynamic_finder_match'
|
||||
autoload :DynamicScopeMatch, 'active_record/dynamic_scope_match'
|
||||
autoload :Migration, 'active_record/migration'
|
||||
autoload :Migrator, 'active_record/migration'
|
||||
autoload :NamedScope, 'active_record/named_scope'
|
||||
autoload :NestedAttributes, 'active_record/nested_attributes'
|
||||
autoload :Observing, 'active_record/observer'
|
||||
autoload :QueryCache, 'active_record/query_cache'
|
||||
autoload :Reflection, 'active_record/reflection'
|
||||
autoload :Schema, 'active_record/schema'
|
||||
autoload :SchemaDumper, 'active_record/schema_dumper'
|
||||
autoload :Serialization, 'active_record/serialization'
|
||||
autoload :SessionStore, 'active_record/session_store'
|
||||
autoload :TestCase, 'active_record/test_case'
|
||||
autoload :Timestamp, 'active_record/timestamp'
|
||||
autoload :Transactions, 'active_record/transactions'
|
||||
autoload :Validations, 'active_record/validations'
|
||||
|
||||
module Locking
|
||||
autoload :Optimistic, 'active_record/locking/optimistic'
|
||||
autoload :Pessimistic, 'active_record/locking/pessimistic'
|
||||
end
|
||||
|
||||
module ConnectionAdapters
|
||||
autoload :AbstractAdapter, 'active_record/connection_adapters/abstract_adapter'
|
||||
end
|
||||
end
|
||||
|
||||
I18n.load_path << File.dirname(__FILE__) + '/active_record/locale/en.yml'
|
||||
|
|
@ -1,261 +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.exchange_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#exchange_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
|
||||
#
|
||||
# == Custom constructors and converters
|
||||
#
|
||||
# By default value objects are initialized by calling the <tt>new</tt> constructor of the value class passing each of the
|
||||
# mapped attributes, in the order specified by the <tt>:mapping</tt> option, as arguments. If the value class doesn't support
|
||||
# this convention then +composed_of+ allows a custom constructor to be specified.
|
||||
#
|
||||
# When a new value is assigned to the value object the default assumption is that the new value is an instance of the value
|
||||
# class. Specifying a custom converter allows the new value to be automatically converted to an instance of value class if
|
||||
# necessary.
|
||||
#
|
||||
# For example, the NetworkResource model has +network_address+ and +cidr_range+ attributes that should be aggregated using the
|
||||
# NetAddr::CIDR value class (http://netaddr.rubyforge.org). The constructor for the value class is called +create+ and it
|
||||
# expects a CIDR address string as a parameter. New values can be assigned to the value object using either another
|
||||
# NetAddr::CIDR object, a string or an array. The <tt>:constructor</tt> and <tt>:converter</tt> options can be used to
|
||||
# meet these requirements:
|
||||
#
|
||||
# class NetworkResource < ActiveRecord::Base
|
||||
# composed_of :cidr,
|
||||
# :class_name => 'NetAddr::CIDR',
|
||||
# :mapping => [ %w(network_address network), %w(cidr_range bits) ],
|
||||
# :allow_nil => true,
|
||||
# :constructor => Proc.new { |network_address, cidr_range| NetAddr::CIDR.create("#{network_address}/#{cidr_range}") },
|
||||
# :converter => Proc.new { |value| NetAddr::CIDR.create(value.is_a?(Array) ? value.join('/') : value) }
|
||||
# end
|
||||
#
|
||||
# # This calls the :constructor
|
||||
# network_resource = NetworkResource.new(:network_address => '192.168.0.1', :cidr_range => 24)
|
||||
#
|
||||
# # These assignments will both use the :converter
|
||||
# network_resource.cidr = [ '192.168.2.1', 8 ]
|
||||
# network_resource.cidr = '192.168.0.1/24'
|
||||
#
|
||||
# # This assignment won't use the :converter as the value is already an instance of the value class
|
||||
# network_resource.cidr = NetAddr::CIDR.create('192.168.2.1/8')
|
||||
#
|
||||
# # Saving and then reloading will use the :constructor on reload
|
||||
# network_resource.save
|
||||
# network_resource.reload
|
||||
#
|
||||
# == 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> - Specifies 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 the mapping of entity attributes to attributes of the value object. Each mapping
|
||||
# is represented as an array where the first item is the name of the entity attribute and the second item is the
|
||||
# name the attribute in the value object. The order in which mappings are defined determine the order in which
|
||||
# attributes are sent to the value class constructor.
|
||||
# * <tt>:allow_nil</tt> - Specifies that the value object will not be instantiated when all mapped
|
||||
# attributes are +nil+. Setting the value object to +nil+ has the effect of writing +nil+ to all mapped attributes.
|
||||
# This defaults to +false+.
|
||||
# * <tt>:constructor</tt> - A symbol specifying the name of the constructor method or a Proc that is called to
|
||||
# initialize the value object. The constructor is passed all of the mapped attributes, in the order that they
|
||||
# are defined in the <tt>:mapping option</tt>, as arguments and uses them to instantiate a <tt>:class_name</tt> object.
|
||||
# The default is <tt>:new</tt>.
|
||||
# * <tt>:converter</tt> - A symbol specifying the name of a class method of <tt>:class_name</tt> or a Proc that is
|
||||
# called when a new value is assigned to the value object. The converter is passed the single value that is used
|
||||
# in the assignment and is only called if the new value is not 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), :converter => Proc.new { |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
|
||||
# composed_of :ip_address,
|
||||
# :class_name => 'IPAddr',
|
||||
# :mapping => %w(ip to_i),
|
||||
# :constructor => Proc.new { |ip| IPAddr.new(ip, Socket::AF_INET) },
|
||||
# :converter => Proc.new { |ip| ip.is_a?(Integer) ? IPAddr.new(ip, Socket::AF_INET) : IPAddr.new(ip.to_s) }
|
||||
#
|
||||
def composed_of(part_id, options = {}, &block)
|
||||
options.assert_valid_keys(:class_name, :mapping, :allow_nil, :constructor, :converter)
|
||||
|
||||
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
|
||||
constructor = options[:constructor] || :new
|
||||
converter = options[:converter] || block
|
||||
|
||||
ActiveSupport::Deprecation.warn('The conversion block has been deprecated, use the :converter option instead.', caller) if block_given?
|
||||
|
||||
reader_method(name, class_name, mapping, allow_nil, constructor)
|
||||
writer_method(name, class_name, mapping, allow_nil, converter)
|
||||
|
||||
create_reflection(:composed_of, part_id, options, self)
|
||||
end
|
||||
|
||||
private
|
||||
def reader_method(name, class_name, mapping, allow_nil, constructor)
|
||||
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? })
|
||||
attrs = mapping.collect {|pair| read_attribute(pair.first)}
|
||||
object = case constructor
|
||||
when Symbol
|
||||
class_name.constantize.send(constructor, *attrs)
|
||||
when Proc, Method
|
||||
constructor.call(*attrs)
|
||||
else
|
||||
raise ArgumentError, 'Constructor must be a symbol denoting the constructor method to call or a Proc to be invoked.'
|
||||
end
|
||||
instance_variable_set("@#{name}", object)
|
||||
end
|
||||
instance_variable_get("@#{name}")
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
def writer_method(name, class_name, mapping, allow_nil, converter)
|
||||
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
|
||||
unless part.is_a?(class_name.constantize) || converter.nil?
|
||||
part = case converter
|
||||
when Symbol
|
||||
class_name.constantize.send(converter, part)
|
||||
when Proc, Method
|
||||
converter.call(part)
|
||||
else
|
||||
raise ArgumentError, 'Converter must be a symbol denoting the converter method to call or a Proc to be invoked.'
|
||||
end
|
||||
end
|
||||
mapping.each { |pair| self[pair.first] = part.send(pair.last) }
|
||||
instance_variable_set("@#{name}", part.freeze)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,406 +0,0 @@
|
|||
module ActiveRecord
|
||||
# See ActiveRecord::AssociationPreload::ClassMethods for documentation.
|
||||
module AssociationPreload #:nodoc:
|
||||
def self.included(base)
|
||||
base.extend(ClassMethods)
|
||||
end
|
||||
|
||||
# Implements the details of eager loading of ActiveRecord associations.
|
||||
# Application developers should not use this module directly.
|
||||
#
|
||||
# ActiveRecord::Base is extended with this module. The source code in
|
||||
# ActiveRecord::Base references methods defined in this module.
|
||||
#
|
||||
# Note that 'eager loading' and 'preloading' are actually the same thing.
|
||||
# However, there are two different eager loading strategies.
|
||||
#
|
||||
# The first one is by using table joins. This was only strategy available
|
||||
# prior to Rails 2.1. Suppose that you have an Author model with columns
|
||||
# 'name' and 'age', and a Book model with columns 'name' and 'sales'. Using
|
||||
# this strategy, ActiveRecord would try to retrieve all data for an author
|
||||
# and all of its books via a single query:
|
||||
#
|
||||
# SELECT * FROM authors
|
||||
# LEFT OUTER JOIN books ON authors.id = books.id
|
||||
# WHERE authors.name = 'Ken Akamatsu'
|
||||
#
|
||||
# However, this could result in many rows that contain redundant data. After
|
||||
# having received the first row, we already have enough data to instantiate
|
||||
# the Author object. In all subsequent rows, only the data for the joined
|
||||
# 'books' table is useful; the joined 'authors' data is just redundant, and
|
||||
# processing this redundant data takes memory and CPU time. The problem
|
||||
# quickly becomes worse and worse as the level of eager loading increases
|
||||
# (i.e. if ActiveRecord is to eager load the associations' assocations as
|
||||
# well).
|
||||
#
|
||||
# The second strategy is to use multiple database queries, one for each
|
||||
# level of association. Since Rails 2.1, this is the default strategy. In
|
||||
# situations where a table join is necessary (e.g. when the +:conditions+
|
||||
# option references an association's column), it will fallback to the table
|
||||
# join strategy.
|
||||
#
|
||||
# See also ActiveRecord::Associations::ClassMethods, which explains eager
|
||||
# loading in a more high-level (application developer-friendly) manner.
|
||||
module ClassMethods
|
||||
protected
|
||||
|
||||
# Eager loads the named associations for the given ActiveRecord record(s).
|
||||
#
|
||||
# In this description, 'association name' shall refer to the name passed
|
||||
# to an association creation method. For example, a model that specifies
|
||||
# <tt>belongs_to :author</tt>, <tt>has_many :buyers</tt> has association
|
||||
# names +:author+ and +:buyers+.
|
||||
#
|
||||
# == Parameters
|
||||
# +records+ is an array of ActiveRecord::Base. This array needs not be flat,
|
||||
# i.e. +records+ itself may also contain arrays of records. In any case,
|
||||
# +preload_associations+ will preload the associations all records by
|
||||
# flattening +records+.
|
||||
#
|
||||
# +associations+ specifies one or more associations that you want to
|
||||
# preload. It may be:
|
||||
# - a Symbol or a String which specifies a single association name. For
|
||||
# example, specifiying +:books+ allows this method to preload all books
|
||||
# for an Author.
|
||||
# - an Array which specifies multiple association names. This array
|
||||
# is processed recursively. For example, specifying <tt>[:avatar, :books]</tt>
|
||||
# allows this method to preload an author's avatar as well as all of his
|
||||
# books.
|
||||
# - a Hash which specifies multiple association names, as well as
|
||||
# association names for the to-be-preloaded association objects. For
|
||||
# example, specifying <tt>{ :author => :avatar }</tt> will preload a
|
||||
# book's author, as well as that author's avatar.
|
||||
#
|
||||
# +:associations+ has the same format as the +:include+ option for
|
||||
# <tt>ActiveRecord::Base.find</tt>. So +associations+ could look like this:
|
||||
#
|
||||
# :books
|
||||
# [ :books, :author ]
|
||||
# { :author => :avatar }
|
||||
# [ :books, { :author => :avatar } ]
|
||||
#
|
||||
# +preload_options+ contains options that will be passed to ActiveRecord#find
|
||||
# (which is called under the hood for preloading records). But it is passed
|
||||
# only one level deep in the +associations+ argument, i.e. it's not passed
|
||||
# to the child associations when +associations+ is a Hash.
|
||||
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.compact
|
||||
unless parents.empty?
|
||||
parents.first.class.preload_associations(parents, child)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Preloads a specific named association for the given records. This is
|
||||
# called by +preload_associations+ as its base case.
|
||||
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
|
||||
# unnecessarily
|
||||
records.group_by {|record| class_to_reflection[record.class] ||= record.class.reflections[association]}.each do |reflection, records|
|
||||
raise ConfigurationError, "Association named '#{ association }' was not found; perhaps you misspelled it?" unless reflection
|
||||
|
||||
# 'reflection.macro' can return 'belongs_to', 'has_many', etc. Thus,
|
||||
# the following could call 'preload_belongs_to_association',
|
||||
# 'preload_has_many_association', etc.
|
||||
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)
|
||||
association_proxy.__send__(:set_inverse_instance, associated_record, parent_record)
|
||||
end
|
||||
end
|
||||
|
||||
def add_preloaded_record_to_collection(parent_records, reflection_name, associated_record)
|
||||
parent_records.each do |parent_record|
|
||||
parent_record.send("set_#{reflection_name}_target", associated_record)
|
||||
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|
|
||||
association_proxy = mapped_record.send("set_#{reflection_name}_target", associated_record)
|
||||
association_proxy.__send__(:set_inverse_instance, associated_record, mapped_record)
|
||||
end
|
||||
end
|
||||
|
||||
id_to_record_map.each do |id, records|
|
||||
next if seen_keys.include?(id.to_s)
|
||||
records.each {|record| record.send("set_#{reflection_name}_target", nil) }
|
||||
end
|
||||
end
|
||||
|
||||
# Given a collection of ActiveRecord objects, constructs a Hash which maps
|
||||
# the objects' IDs to the relevant objects. Returns a 2-tuple
|
||||
# <tt>(id_to_record_map, ids)</tt> where +id_to_record_map+ is the Hash,
|
||||
# and +ids+ is an Array of record IDs.
|
||||
def construct_id_map(records, primary_key=nil)
|
||||
id_to_record_map = {}
|
||||
ids = []
|
||||
records.each do |record|
|
||||
primary_key ||= record.class.primary_key
|
||||
ids << record[primary_key]
|
||||
mapped_records = (id_to_record_map[ids.last.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_or_equals_for_ids(ids)}"
|
||||
conditions << append_conditions(reflection, preload_options)
|
||||
|
||||
associated_records = reflection.klass.with_exclusive_scope do
|
||||
reflection.klass.find(:all, :conditions => [conditions, ids],
|
||||
:include => options[:include],
|
||||
:joins => "INNER JOIN #{connection.quote_table_name options[:join_table]} t0 ON #{reflection.klass.quoted_table_name}.#{reflection.klass.primary_key} = t0.#{reflection.association_foreign_key}",
|
||||
:select => "#{options[:select] || table_name+'.*'}, t0.#{reflection.primary_key_name} as the_parent_record_id",
|
||||
:order => options[:order])
|
||||
end
|
||||
set_association_collection_records(id_to_record_map, reflection.name, associated_records, 'the_parent_record_id')
|
||||
end
|
||||
|
||||
def preload_has_one_association(records, reflection, preload_options={})
|
||||
return if records.first.send("loaded_#{reflection.name}?")
|
||||
id_to_record_map, ids = construct_id_map(records, reflection.options[:primary_key])
|
||||
options = reflection.options
|
||||
records.each {|record| record.send("set_#{reflection.name}_target", nil)}
|
||||
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
|
||||
through_records.first.class.preload_associations(through_records, source)
|
||||
if through_reflection.macro == :belongs_to
|
||||
rev_id_to_record_map, rev_ids = construct_id_map(records, through_primary_key)
|
||||
rev_primary_key = through_reflection.klass.primary_key
|
||||
through_records.each do |through_record|
|
||||
add_preloaded_record_to_collection(rev_id_to_record_map[through_record[rev_primary_key].to_s],
|
||||
reflection.name, through_record.send(source))
|
||||
end
|
||||
else
|
||||
through_records.each do |through_record|
|
||||
add_preloaded_record_to_collection(id_to_record_map[through_record[through_primary_key].to_s],
|
||||
reflection.name, through_record.send(source))
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
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={})
|
||||
return if records.first.send(reflection.name).loaded?
|
||||
options = reflection.options
|
||||
|
||||
primary_key_name = reflection.through_reflection_primary_key_name
|
||||
id_to_record_map, ids = construct_id_map(records, primary_key_name || reflection.options[:primary_key])
|
||||
records.each {|record| record.send(reflection.name).loaded}
|
||||
|
||||
if options[:through]
|
||||
through_records = preload_through_records(records, reflection, options[:through])
|
||||
through_reflection = reflections[options[:through]]
|
||||
unless through_records.empty?
|
||||
source = reflection.source_reflection.name
|
||||
through_records.first.class.preload_associations(through_records, source, options)
|
||||
through_records.each do |through_record|
|
||||
through_record_id = through_record[reflection.through_reflection_primary_key].to_s
|
||||
add_preloaded_records_to_collection(id_to_record_map[through_record_id], 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
|
||||
options = {}
|
||||
options[:include] = reflection.options[:include] || reflection.options[:source] if reflection.options[:conditions]
|
||||
options[:order] = reflection.options[:order]
|
||||
options[:conditions] = reflection.options[:conditions]
|
||||
records.first.class.preload_associations(records, through_association, options)
|
||||
through_records = records.map {|record| record.send(through_association)}.flatten
|
||||
end
|
||||
through_records.compact!
|
||||
through_records
|
||||
end
|
||||
|
||||
def preload_belongs_to_association(records, reflection, preload_options={})
|
||||
return if records.first.send("loaded_#{reflection.name}?")
|
||||
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
|
||||
next if id_map.empty?
|
||||
klass = klass_name.constantize
|
||||
|
||||
table_name = klass.quoted_table_name
|
||||
primary_key = reflection.options[:primary_key] || klass.primary_key
|
||||
column_type = klass.columns.detect{|c| c.name == primary_key}.type
|
||||
ids = id_map.keys.map do |id|
|
||||
if column_type == :integer
|
||||
id.to_i
|
||||
elsif column_type == :float
|
||||
id.to_f
|
||||
else
|
||||
id
|
||||
end
|
||||
end
|
||||
conditions = "#{table_name}.#{connection.quote_column_name(primary_key)} #{in_or_equals_for_ids(ids)}"
|
||||
conditions << append_conditions(reflection, preload_options)
|
||||
associated_records = klass.with_exclusive_scope do
|
||||
klass.find(:all, :conditions => [conditions, ids],
|
||||
:include => options[:include],
|
||||
:select => options[:select],
|
||||
:joins => options[:joins],
|
||||
:order => options[:order])
|
||||
end
|
||||
set_association_single_records(id_map, reflection.name, associated_records, primary_key)
|
||||
end
|
||||
end
|
||||
|
||||
def find_associated_records(ids, reflection, preload_options)
|
||||
options = reflection.options
|
||||
table_name = reflection.klass.quoted_table_name
|
||||
|
||||
if interface = reflection.options[:as]
|
||||
parent_type = if reflection.active_record.abstract_class?
|
||||
self.base_class.sti_name
|
||||
else
|
||||
reflection.active_record.sti_name
|
||||
end
|
||||
|
||||
conditions = "#{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_id"} #{in_or_equals_for_ids(ids)} and #{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_type"} = '#{parent_type}'"
|
||||
else
|
||||
foreign_key = reflection.primary_key_name
|
||||
conditions = "#{reflection.klass.quoted_table_name}.#{foreign_key} #{in_or_equals_for_ids(ids)}"
|
||||
end
|
||||
|
||||
conditions << append_conditions(reflection, preload_options)
|
||||
|
||||
reflection.klass.with_exclusive_scope do
|
||||
reflection.klass.find(:all,
|
||||
:select => (preload_options[:select] || options[:select] || "#{table_name}.*"),
|
||||
:include => preload_options[:include] || options[:include],
|
||||
:conditions => [conditions, ids],
|
||||
:joins => options[:joins],
|
||||
:group => preload_options[:group] || options[:group],
|
||||
:order => preload_options[:order] || options[:order])
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def interpolate_sql_for_preload(sql)
|
||||
instance_eval("%@#{sql.gsub('@', '\@')}@")
|
||||
end
|
||||
|
||||
def append_conditions(reflection, preload_options)
|
||||
sql = ""
|
||||
sql << " AND (#{interpolate_sql_for_preload(reflection.sanitized_conditions)})" if reflection.sanitized_conditions
|
||||
sql << " AND (#{sanitize_sql preload_options[:conditions]})" if preload_options[:conditions]
|
||||
sql
|
||||
end
|
||||
|
||||
def in_or_equals_for_ids(ids)
|
||||
ids.size > 1 ? "IN (?)" : "= ?"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,508 +0,0 @@
|
|||
require 'set'
|
||||
|
||||
module ActiveRecord
|
||||
module Associations
|
||||
# AssociationCollection is an abstract class that provides common stuff to
|
||||
# ease the implementation of association proxies that represent
|
||||
# collections. See the class hierarchy in AssociationProxy.
|
||||
#
|
||||
# You need to be careful with assumptions regarding the target: The proxy
|
||||
# does not fetch records from the database until it needs them, but new
|
||||
# ones created with +build+ are added to the target. So, the target may be
|
||||
# non-empty and still lack children waiting to be read from the database.
|
||||
# If you look directly to the database you cannot assume that's the entire
|
||||
# collection because new records may have beed added to the target, etc.
|
||||
#
|
||||
# If you need to work on all current children, new and existing records,
|
||||
# +load_target+ and the +loaded+ flag are your friends.
|
||||
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 { |arg| arg.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
|
||||
if @target.is_a?(Array)
|
||||
@target.to_ary
|
||||
else
|
||||
Array(@target)
|
||||
end
|
||||
end
|
||||
|
||||
def reset
|
||||
reset_target!
|
||||
@loaded = false
|
||||
end
|
||||
|
||||
def build(attributes = {}, &block)
|
||||
if attributes.is_a?(Array)
|
||||
attributes.collect { |attr| build(attr, &block) }
|
||||
else
|
||||
build_record(attributes) do |record|
|
||||
block.call(record) if block_given?
|
||||
set_belongs_to_association_for(record)
|
||||
end
|
||||
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?
|
||||
|
||||
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, :<<
|
||||
|
||||
# Starts a transaction in the association class's database connection.
|
||||
#
|
||||
# class Author < ActiveRecord::Base
|
||||
# has_many :books
|
||||
# end
|
||||
#
|
||||
# Author.find(:first).books.transaction do
|
||||
# # same effect as calling Book.transaction
|
||||
# end
|
||||
def transaction(*args)
|
||||
@reflection.klass.transaction(*args) do
|
||||
yield
|
||||
end
|
||||
end
|
||||
|
||||
# Remove all records from this association
|
||||
#
|
||||
# See delete for more info.
|
||||
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
|
||||
|
||||
# Count all records using SQL. If the +:counter_sql+ option is set for the association, it will
|
||||
# be used for the query. If no +:counter_sql+ was supplied, but +:finder_sql+ was set, the
|
||||
# descendant's +construct_sql+ method will have set :counter_sql automatically.
|
||||
# Otherwise, construct options and pass them with scope to the target class's +count+.
|
||||
def count(*args)
|
||||
if @reflection.options[:counter_sql]
|
||||
@reflection.klass.count_by_sql(@counter_sql)
|
||||
else
|
||||
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.
|
||||
column_name = "#{@reflection.quoted_table_name}.#{@reflection.klass.primary_key}" if column_name == :all
|
||||
options.merge!(:distinct => true)
|
||||
end
|
||||
|
||||
value = @reflection.klass.send(:with_scope, construct_scope) { @reflection.klass.count(column_name, options) }
|
||||
|
||||
limit = @reflection.options[:limit]
|
||||
offset = @reflection.options[:offset]
|
||||
|
||||
if limit || offset
|
||||
[ [value - offset.to_i, 0].max, limit.to_i ].min
|
||||
else
|
||||
value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Removes +records+ from this association calling +before_remove+ and
|
||||
# +after_remove+ callbacks.
|
||||
#
|
||||
# This method is abstract in the sense that +delete_records+ has to be
|
||||
# provided by descendants. Note this method does not imply the records
|
||||
# are actually removed from the database, that depends precisely on
|
||||
# +delete_records+. They are in any case removed from the collection.
|
||||
def delete(*records)
|
||||
remove_records(records) do |records, old_records|
|
||||
delete_records(old_records) if old_records.any?
|
||||
records.each { |record| @target.delete(record) }
|
||||
end
|
||||
end
|
||||
|
||||
# Destroy +records+ and remove them from this association calling
|
||||
# +before_remove+ and +after_remove+ callbacks.
|
||||
#
|
||||
# Note that this method will _always_ remove records from the database
|
||||
# ignoring the +:dependent+ option.
|
||||
def destroy(*records)
|
||||
records = find(records) if records.any? {|record| record.kind_of?(Fixnum) || record.kind_of?(String)}
|
||||
remove_records(records) do |records, old_records|
|
||||
old_records.each { |record| record.destroy }
|
||||
end
|
||||
|
||||
load_target
|
||||
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
|
||||
|
||||
# Destory all the records from this association.
|
||||
#
|
||||
# See destroy for more info.
|
||||
def destroy_all
|
||||
load_target
|
||||
destroy(@target).tap do
|
||||
reset_target!
|
||||
end
|
||||
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
|
||||
# <tt>collection.size</tt> if it has.
|
||||
#
|
||||
# If the collection has been already loaded +size+ and +length+ are
|
||||
# equivalent. If not and you are going to need the records anyway
|
||||
# +length+ will take one less query. Otherwise +size+ is more efficient.
|
||||
#
|
||||
# This method is abstract in the sense that it relies on
|
||||
# +count_records+, which is a method descendants have to provide.
|
||||
def size
|
||||
if @owner.new_record? || (loaded? && !@reflection.options[:uniq])
|
||||
@target.size
|
||||
elsif !loaded? && @reflection.options[:group]
|
||||
load_target.size
|
||||
elsif !loaded? && !@reflection.options[:uniq] && @target.is_a?(Array)
|
||||
unsaved_records = @target.select { |r| r.new_record? }
|
||||
unsaved_records.size + count_records
|
||||
else
|
||||
count_records
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the size of the collection calling +size+ on the target.
|
||||
#
|
||||
# If the collection has been already loaded +length+ and +size+ are
|
||||
# equivalent. If not and you are going to need the records anyway this
|
||||
# method will take one less query. Otherwise +size+ is more efficient.
|
||||
def length
|
||||
load_target.size
|
||||
end
|
||||
|
||||
# Equivalent to <tt>collection.size.zero?</tt>. If the collection has
|
||||
# not been already loaded and you are going to fetch the records anyway
|
||||
# it is better to check <tt>collection.length.zero?</tt>.
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
def proxy_respond_to?(method, include_private = false)
|
||||
super || @reflection.klass.respond_to?(method, include_private)
|
||||
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.map do |f|
|
||||
i = @target.index(f)
|
||||
if i
|
||||
@target.delete_at(i).tap do |t|
|
||||
keys = ["id"] + t.changes.keys + (f.attribute_names - t.attribute_names)
|
||||
t.attributes = f.attributes.except(*keys)
|
||||
end
|
||||
else
|
||||
f
|
||||
end
|
||||
end + @target
|
||||
else
|
||||
@target = find_target
|
||||
end
|
||||
end
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
reset
|
||||
end
|
||||
end
|
||||
|
||||
loaded if target
|
||||
target
|
||||
end
|
||||
|
||||
def method_missing(method, *args)
|
||||
case method.to_s
|
||||
when 'find_or_create'
|
||||
return find(:first, :conditions => args.first) || create(args.first)
|
||||
when /^find_or_create_by_(.*)$/
|
||||
rest = $1
|
||||
return send("find_by_#{rest}", *args) ||
|
||||
method_missing("create_by_#{rest}", *args)
|
||||
when /^create_by_(.*)$/
|
||||
return create($1.split('_and_').zip(args).inject({}) { |h,kv| k,v=kv ; h[k] = v ; h })
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
records = @reflection.options[:uniq] ? uniq(records) : records
|
||||
records.each do |record|
|
||||
set_inverse_instance(record, @owner)
|
||||
end
|
||||
records
|
||||
end
|
||||
|
||||
def add_record_to_target_with_callbacks(record)
|
||||
callback(:before_add, record)
|
||||
yield(record) if block_given?
|
||||
@target ||= [] unless loaded?
|
||||
index = @target.index(record)
|
||||
unless @reflection.options[:uniq] && index
|
||||
if index
|
||||
@target[index] = record
|
||||
else
|
||||
@target << record
|
||||
end
|
||||
end
|
||||
callback(:after_add, record)
|
||||
set_inverse_instance(record, @owner)
|
||||
record
|
||||
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]) do
|
||||
@reflection.build_association(attrs)
|
||||
end
|
||||
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.build_association(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 remove_records(*records)
|
||||
records = flatten_deeper(records)
|
||||
records.each { |record| raise_on_type_mismatch(record) }
|
||||
|
||||
transaction do
|
||||
records.each { |record| callback(:before_remove, record) }
|
||||
old_records = records.reject { |r| r.new_record? }
|
||||
yield(records, old_records)
|
||||
records.each { |record| callback(:after_remove, record) }
|
||||
end
|
||||
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.any? { |record| record.new_record? } || args.first.kind_of?(Integer))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,288 +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
|
||||
reflection.check_validity!
|
||||
Array(reflection.options[:extend]).each { |ext| proxy_extend(ext) }
|
||||
reset
|
||||
end
|
||||
|
||||
# Returns the owner of the proxy.
|
||||
def proxy_owner
|
||||
@owner
|
||||
end
|
||||
|
||||
# Returns the reflection object that represents the association handled
|
||||
# by the proxy.
|
||||
def proxy_reflection
|
||||
@reflection
|
||||
end
|
||||
|
||||
# Returns the \target of the proxy, same as +target+.
|
||||
def proxy_target
|
||||
@target
|
||||
end
|
||||
|
||||
# Does the proxy or its \target respond to +symbol+?
|
||||
def respond_to?(*args)
|
||||
proxy_respond_to?(*args) || (load_target && @target.respond_to?(*args))
|
||||
end
|
||||
|
||||
# Forwards <tt>===</tt> explicitly to the \target because the instance method
|
||||
# removal above doesn't catch it. Loads the \target if needed.
|
||||
def ===(other)
|
||||
load_target
|
||||
other === @target
|
||||
end
|
||||
|
||||
# Returns the name of the table of the related class:
|
||||
#
|
||||
# post.comments.aliased_table_name # => "comments"
|
||||
#
|
||||
def aliased_table_name
|
||||
@reflection.klass.table_name
|
||||
end
|
||||
|
||||
# Returns the SQL string that corresponds to the <tt>:conditions</tt>
|
||||
# option of the macro, if given, or +nil+ otherwise.
|
||||
def conditions
|
||||
@conditions ||= interpolate_sql(@reflection.sanitized_conditions) if @reflection.sanitized_conditions
|
||||
end
|
||||
alias :sql_conditions :conditions
|
||||
|
||||
# Resets the \loaded flag to +false+ and sets the \target to +nil+.
|
||||
def reset
|
||||
@loaded = false
|
||||
@target = nil
|
||||
end
|
||||
|
||||
# Reloads the \target and returns +self+ on success.
|
||||
def reload
|
||||
reset
|
||||
load_target
|
||||
self unless @target.nil?
|
||||
end
|
||||
|
||||
# Has the \target been already \loaded?
|
||||
def loaded?
|
||||
@loaded
|
||||
end
|
||||
|
||||
# Asserts the \target has been loaded setting the \loaded flag to +true+.
|
||||
def loaded
|
||||
@loaded = true
|
||||
end
|
||||
|
||||
# Returns the target of this proxy, same as +proxy_target+.
|
||||
def target
|
||||
@target
|
||||
end
|
||||
|
||||
# Sets the target of this proxy to <tt>\target</tt>, and the \loaded flag to +true+.
|
||||
def target=(target)
|
||||
@target = target
|
||||
loaded
|
||||
end
|
||||
|
||||
# Forwards the call to the target. Loads the \target if needed.
|
||||
def inspect
|
||||
load_target
|
||||
@target.inspect
|
||||
end
|
||||
|
||||
def send(method, *args)
|
||||
if proxy_respond_to?(method)
|
||||
super
|
||||
else
|
||||
load_target
|
||||
@target.send(method, *args)
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
# Does the association have a <tt>:dependent</tt> option?
|
||||
def dependent?
|
||||
@reflection.options[:dependent]
|
||||
end
|
||||
|
||||
# Returns a string with the IDs of +records+ joined with a comma, quoted
|
||||
# if needed. The result is ready to be inserted into a SQL IN clause.
|
||||
#
|
||||
# quoted_record_ids(records) # => "23,56,58,67"
|
||||
#
|
||||
def quoted_record_ids(records)
|
||||
records.map { |record| record.quoted_id }.join(',')
|
||||
end
|
||||
|
||||
def interpolate_sql(sql, record = nil)
|
||||
@owner.send(:interpolate_sql, sql, record)
|
||||
end
|
||||
|
||||
# Forwards the call to the reflection class.
|
||||
def sanitize_sql(sql, table_name = @reflection.klass.quoted_table_name)
|
||||
@reflection.klass.send(:sanitize_sql, sql, table_name)
|
||||
end
|
||||
|
||||
# Assigns the ID of the owner to the corresponding foreign key in +record+.
|
||||
# If the association is polymorphic the type of the owner is also set.
|
||||
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
|
||||
unless @owner.new_record?
|
||||
primary_key = @reflection.options[:primary_key] || :id
|
||||
record[@reflection.primary_key_name] = @owner.send(primary_key)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Merges into +options+ the ones coming from the reflection.
|
||||
def merge_options_from_reflection!(options)
|
||||
options.reverse_merge!(
|
||||
:group => @reflection.options[:group],
|
||||
:having => @reflection.options[:having],
|
||||
:limit => @reflection.options[:limit],
|
||||
:offset => @reflection.options[:offset],
|
||||
:joins => @reflection.options[:joins],
|
||||
:include => @reflection.options[:include],
|
||||
:select => @reflection.options[:select],
|
||||
:readonly => @reflection.options[:readonly]
|
||||
)
|
||||
end
|
||||
|
||||
# Forwards +with_scope+ to the reflection.
|
||||
def with_scope(*args, &block)
|
||||
@reflection.klass.send :with_scope, *args, &block
|
||||
end
|
||||
|
||||
private
|
||||
# Forwards any missing method call to the \target.
|
||||
def method_missing(method, *args, &block)
|
||||
if load_target
|
||||
if @target.respond_to?(method)
|
||||
@target.send(method, *args, &block)
|
||||
else
|
||||
super
|
||||
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 (both vanilla and polymorphic).
|
||||
def foreign_key_present
|
||||
false
|
||||
end
|
||||
|
||||
# Raises ActiveRecord::AssociationTypeMismatch unless +record+ is of
|
||||
# the kind of the class of the associated objects. Meant to be used as
|
||||
# a sanity check when you are about to assign an associated record.
|
||||
def raise_on_type_mismatch(record)
|
||||
unless record.is_a?(@reflection.klass) || record.is_a?(@reflection.class_name.constantize)
|
||||
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.is_a?(Hash)) ? element.flatten : element }.flatten
|
||||
end
|
||||
|
||||
# Returns the ID of the owner, quoted if needed.
|
||||
def owner_quoted_id
|
||||
@owner.quoted_id
|
||||
end
|
||||
|
||||
def set_inverse_instance(record, instance)
|
||||
return if record.nil? || !we_can_set_the_inverse_on_this?(record)
|
||||
inverse_relationship = @reflection.inverse_of
|
||||
unless inverse_relationship.nil?
|
||||
record.send(:"set_#{inverse_relationship.name}_target", instance)
|
||||
end
|
||||
end
|
||||
|
||||
# Override in subclasses
|
||||
def we_can_set_the_inverse_on_this?(record)
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
module ActiveRecord
|
||||
module Associations
|
||||
class BelongsToAssociation < AssociationProxy #:nodoc:
|
||||
def create(attributes = {})
|
||||
replace(@reflection.create_association(attributes))
|
||||
end
|
||||
|
||||
def build(attributes = {})
|
||||
replace(@reflection.build_association(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, previous_record_id) 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(record) unless record.new_record?
|
||||
@updated = true
|
||||
end
|
||||
|
||||
set_inverse_instance(record, @owner)
|
||||
|
||||
loaded
|
||||
record
|
||||
end
|
||||
|
||||
def updated?
|
||||
@updated
|
||||
end
|
||||
|
||||
private
|
||||
def find_target
|
||||
find_method = if @reflection.options[:primary_key]
|
||||
"find_by_#{@reflection.options[:primary_key]}"
|
||||
else
|
||||
"find"
|
||||
end
|
||||
the_target = @reflection.klass.send(find_method,
|
||||
@owner[@reflection.primary_key_name],
|
||||
:select => @reflection.options[:select],
|
||||
:conditions => conditions,
|
||||
:include => @reflection.options[:include],
|
||||
:readonly => @reflection.options[:readonly]
|
||||
) if @owner[@reflection.primary_key_name]
|
||||
set_inverse_instance(the_target, @owner)
|
||||
the_target
|
||||
end
|
||||
|
||||
def foreign_key_present
|
||||
!@owner[@reflection.primary_key_name].nil?
|
||||
end
|
||||
|
||||
def record_id(record)
|
||||
record.send(@reflection.options[:primary_key] || :id)
|
||||
end
|
||||
|
||||
def previous_record_id
|
||||
@previous_record_id ||= if @reflection.options[:primary_key]
|
||||
previous_record = @owner.send(@reflection.name)
|
||||
previous_record.nil? ? nil : previous_record.id
|
||||
else
|
||||
@owner[@reflection.primary_key_name]
|
||||
end
|
||||
end
|
||||
|
||||
# NOTE - for now, we're only supporting inverse setting from belongs_to back onto
|
||||
# has_one associations.
|
||||
def we_can_set_the_inverse_on_this?(record)
|
||||
@reflection.has_inverse? && @reflection.inverse_of.macro == :has_one
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,77 +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(record)
|
||||
@owner[@reflection.options[:foreign_type]] = record.class.base_class.name.to_s
|
||||
|
||||
@updated = true
|
||||
end
|
||||
|
||||
set_inverse_instance(record, @owner)
|
||||
loaded
|
||||
record
|
||||
end
|
||||
|
||||
def updated?
|
||||
@updated
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# NOTE - for now, we're only supporting inverse setting from belongs_to back onto
|
||||
# has_one associations.
|
||||
def we_can_set_the_inverse_on_this?(record)
|
||||
if @reflection.has_inverse?
|
||||
inverse_association = @reflection.polymorphic_inverse_of(record.class)
|
||||
inverse_association && inverse_association.macro == :has_one
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
def set_inverse_instance(record, instance)
|
||||
return if record.nil? || !we_can_set_the_inverse_on_this?(record)
|
||||
inverse_relationship = @reflection.polymorphic_inverse_of(record.class)
|
||||
unless inverse_relationship.nil?
|
||||
record.send(:"set_#{inverse_relationship.name}_target", instance)
|
||||
end
|
||||
end
|
||||
|
||||
def find_target
|
||||
return nil if association_class.nil?
|
||||
|
||||
target =
|
||||
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
|
||||
set_inverse_instance(target, @owner)
|
||||
target
|
||||
end
|
||||
|
||||
def foreign_key_present
|
||||
!@owner[@reflection.primary_key_name].nil?
|
||||
end
|
||||
|
||||
def record_id(record)
|
||||
record.send(@reflection.options[:primary_key] || :id)
|
||||
end
|
||||
|
||||
def association_class
|
||||
@owner[@reflection.options[:foreign_type]] ? @owner[@reflection.options[:foreign_type]].constantize : nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,143 +0,0 @@
|
|||
module ActiveRecord
|
||||
module Associations
|
||||
class HasAndBelongsToManyAssociation < AssociationCollection #:nodoc:
|
||||
def initialize(owner, reflection)
|
||||
super
|
||||
@primary_key_list = {}
|
||||
end
|
||||
|
||||
def create(attributes = {})
|
||||
create_record(attributes) { |record| insert_record(record) }
|
||||
end
|
||||
|
||||
def create!(attributes = {})
|
||||
create_record(attributes) { |record| insert_record(record, true) }
|
||||
end
|
||||
|
||||
def columns
|
||||
@reflection.columns(@reflection.options[:join_table], "#{@reflection.options[:join_table]} Columns")
|
||||
end
|
||||
|
||||
def reset_column_information
|
||||
@reflection.reset_column_information
|
||||
end
|
||||
|
||||
def has_primary_key?
|
||||
return @has_primary_key unless @has_primary_key.nil?
|
||||
@has_primary_key = (@owner.connection.supports_primary_key? &&
|
||||
@owner.connection.primary_key(@reflection.options[:join_table]))
|
||||
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, validate = true)
|
||||
if has_primary_key?
|
||||
raise ActiveRecord::ConfigurationError,
|
||||
"Primary key is not allowed in a has_and_belongs_to_many join table (#{@reflection.options[:join_table]})."
|
||||
end
|
||||
|
||||
if record.new_record?
|
||||
if force
|
||||
record.save!
|
||||
else
|
||||
return false unless record.save(validate)
|
||||
end
|
||||
end
|
||||
|
||||
if @reflection.options[:insert_sql]
|
||||
@owner.connection.insert(interpolate_sql(@reflection.options[:insert_sql], record))
|
||||
else
|
||||
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
|
||||
if @reflection.options[:finder_sql]
|
||||
@finder_sql = interpolate_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}"
|
||||
|
||||
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
|
||||
{ :find => { :conditions => @finder_sql,
|
||||
:joins => @join_sql,
|
||||
:readonly => false,
|
||||
:order => @reflection.options[:order],
|
||||
:include => @reflection.options[:include],
|
||||
: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 && 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,127 +0,0 @@
|
|||
module ActiveRecord
|
||||
module Associations
|
||||
# This is the proxy that handles a has many association.
|
||||
#
|
||||
# If the association has a <tt>:through</tt> option further specialization
|
||||
# is provided by its child HasManyThroughAssociation.
|
||||
class HasManyAssociation < AssociationCollection #:nodoc:
|
||||
protected
|
||||
def owner_quoted_id
|
||||
if @reflection.options[:primary_key]
|
||||
quote_value(@owner.send(@reflection.options[:primary_key]))
|
||||
else
|
||||
@owner.quoted_id
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the number of records in this collection.
|
||||
#
|
||||
# If the association has a counter cache it gets that value. Otherwise
|
||||
# it will attempt to do a count via SQL, bounded to <tt>:limit</tt> if
|
||||
# there's one. Some configuration options like :group make it impossible
|
||||
# to do a SQL count, in those cases the array count will be used.
|
||||
#
|
||||
# That does not depend on whether the collection has already been loaded
|
||||
# or not. The +size+ method is the one that takes the loaded flag into
|
||||
# account and delegates to +count_records+ if needed.
|
||||
#
|
||||
# If the collection is empty the target is set to an empty array and
|
||||
# the loaded flag is set to true as well.
|
||||
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
|
||||
|
||||
# If there's nothing in the database and @target has no new records
|
||||
# we are certain the current target is an empty array. This is a
|
||||
# documented side-effect of the method that may avoid an extra SELECT.
|
||||
@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, force = false, validate = true)
|
||||
set_belongs_to_association_for(record)
|
||||
force ? record.save! : record.save(validate)
|
||||
end
|
||||
|
||||
# Deletes the records according to the <tt>:dependent</tt> option.
|
||||
def delete_records(records)
|
||||
case @reflection.options[:dependent]
|
||||
when :destroy
|
||||
records.each { |r| r.destroy }
|
||||
when :delete_all
|
||||
@reflection.klass.delete(records.map { |record| record.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})"
|
||||
)
|
||||
@owner.class.update_counters(@owner.id, cached_counter_attribute_name => -records.size) if has_cached_counter?
|
||||
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], :include => @reflection.options[:include]},
|
||||
:create => create_scoping
|
||||
}
|
||||
end
|
||||
|
||||
def we_can_set_the_inverse_on_this?(record)
|
||||
inverse = @reflection.inverse_of
|
||||
return !inverse.nil?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,266 +0,0 @@
|
|||
module ActiveRecord
|
||||
module Associations
|
||||
class HasManyThroughAssociation < HasManyAssociation #:nodoc:
|
||||
alias_method :new, :build
|
||||
|
||||
def create!(attrs = nil)
|
||||
transaction do
|
||||
self << (object = attrs ? @reflection.klass.send(:with_scope, :create => attrs) { @reflection.create_association! } : @reflection.create_association!)
|
||||
object
|
||||
end
|
||||
end
|
||||
|
||||
def create(attrs = nil)
|
||||
transaction do
|
||||
object = if attrs
|
||||
@reflection.klass.send(:with_scope, :create => attrs) {
|
||||
@reflection.create_association
|
||||
}
|
||||
else
|
||||
@reflection.create_association
|
||||
end
|
||||
raise_on_type_mismatch(object)
|
||||
add_record_to_target_with_callbacks(object) do |r|
|
||||
insert_record(object, false)
|
||||
end
|
||||
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 fewer 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
|
||||
|
||||
protected
|
||||
def target_reflection_has_associated_record?
|
||||
if @reflection.through_reflection.macro == :belongs_to && @owner[@reflection.through_reflection.primary_key_name].blank?
|
||||
false
|
||||
else
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
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? && @reflection.source_reflection.options[:include]
|
||||
end
|
||||
|
||||
def insert_record(record, force = true, validate = true)
|
||||
if record.new_record?
|
||||
if force
|
||||
record.save!
|
||||
else
|
||||
return false unless record.save(validate)
|
||||
end
|
||||
end
|
||||
through_reflection = @reflection.through_reflection
|
||||
klass = through_reflection.klass
|
||||
@owner.send(@reflection.through_reflection.name).proxy_target << klass.send(:with_scope, :create => construct_join_attributes(record)) { through_reflection.create_association! }
|
||||
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
|
||||
return [] unless target_reflection_has_associated_record?
|
||||
@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::HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(@owner, @reflection) if [:has_one, :has_many].include?(@reflection.source_reflection.macro)
|
||||
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"]) }
|
||||
elsif reflection.macro == :belongs_to
|
||||
{ reflection.klass.primary_key => @owner[reflection.primary_key_name] }
|
||||
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.through_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.quoted_table_name,
|
||||
@reflection.quoted_table_name, reflection_primary_key,
|
||||
@reflection.through_reflection.quoted_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.klass.send(:type_condition)
|
||||
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
|
||||
|
||||
# NOTE - not sure that we can actually cope with inverses here
|
||||
def we_can_set_the_inverse_on_this?(record)
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,142 +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) do |reflection|
|
||||
attrs = merge_with_conditions(attrs)
|
||||
reflection.create_association(attrs)
|
||||
end
|
||||
end
|
||||
|
||||
def create!(attrs = {}, replace_existing = true)
|
||||
new_record(replace_existing) do |reflection|
|
||||
attrs = merge_with_conditions(attrs)
|
||||
reflection.create_association!(attrs)
|
||||
end
|
||||
end
|
||||
|
||||
def build(attrs = {}, replace_existing = true)
|
||||
new_record(replace_existing) do |reflection|
|
||||
attrs = merge_with_conditions(attrs)
|
||||
reflection.build_association(attrs)
|
||||
end
|
||||
end
|
||||
|
||||
def replace(obj, dont_save = false)
|
||||
load_target
|
||||
|
||||
unless @target.nil? || @target == obj
|
||||
if dependent? && !dont_save
|
||||
case @reflection.options[:dependent]
|
||||
when :delete
|
||||
@target.delete unless @target.new_record?
|
||||
@owner.clear_association_cache
|
||||
when :destroy
|
||||
@target.destroy unless @target.new_record?
|
||||
@owner.clear_association_cache
|
||||
when :nullify
|
||||
@target[@reflection.primary_key_name] = nil
|
||||
@target.save unless @owner.new_record? || @target.new_record?
|
||||
end
|
||||
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
|
||||
|
||||
set_inverse_instance(obj, @owner)
|
||||
@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
|
||||
|
||||
protected
|
||||
def owner_quoted_id
|
||||
if @reflection.options[:primary_key]
|
||||
@owner.class.quote_value(@owner.send(@reflection.options[:primary_key]))
|
||||
else
|
||||
@owner.quoted_id
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def find_target
|
||||
the_target = @reflection.klass.find(:first,
|
||||
:conditions => @finder_sql,
|
||||
:select => @reflection.options[:select],
|
||||
:order => @reflection.options[:order],
|
||||
:include => @reflection.options[:include],
|
||||
:readonly => @reflection.options[:readonly]
|
||||
)
|
||||
set_inverse_instance(the_target, @owner)
|
||||
the_target
|
||||
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]) do
|
||||
yield @reflection
|
||||
end
|
||||
|
||||
if replace_existing
|
||||
replace(record, true)
|
||||
else
|
||||
record[@reflection.primary_key_name] = @owner.id unless @owner.new_record?
|
||||
self.target = record
|
||||
set_inverse_instance(record, @owner)
|
||||
end
|
||||
|
||||
record
|
||||
end
|
||||
|
||||
def merge_with_conditions(attrs={})
|
||||
attrs ||= {}
|
||||
attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash)
|
||||
attrs
|
||||
end
|
||||
|
||||
def we_can_set_the_inverse_on_this?(record)
|
||||
inverse = @reflection.inverse_of
|
||||
return !inverse.nil?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,37 +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
|
||||
new_value ? current_object.update_attributes(construct_join_attributes(new_value)) : current_object.destroy
|
||||
elsif new_value
|
||||
if @owner.new_record?
|
||||
self.target = new_value
|
||||
through_association = @owner.send(:association_instance_get, @reflection.through_reflection.name)
|
||||
through_association.build(construct_join_attributes(new_value))
|
||||
else
|
||||
@owner.send(@reflection.through_reflection.name, klass.create(construct_join_attributes(new_value)))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def find(*args)
|
||||
super(args.merge(:limit => 1))
|
||||
end
|
||||
|
||||
def find_target
|
||||
super.first
|
||||
end
|
||||
|
||||
def reset_target!
|
||||
@target = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,388 +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.class_inheritable_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__)
|
||||
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 self.class.private_method_defined?(method_name)
|
||||
raise NoMethodError.new("Attempt to call private method", method_name, args)
|
||||
end
|
||||
|
||||
# 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
|
||||
return false if ActiveRecord::ConnectionAdapters::Column::FALSE_VALUES.include?(value)
|
||||
!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_private_methods = false)
|
||||
method_name = method.to_s
|
||||
if super
|
||||
return true
|
||||
elsif !include_private_methods && super(method, true)
|
||||
# If we're here than we haven't found among non-private methods
|
||||
# but found among all methods. Which means that given method is private.
|
||||
return false
|
||||
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
|
||||
|
|
@ -1,395 +0,0 @@
|
|||
module ActiveRecord
|
||||
# AutosaveAssociation is a module that takes care of automatically saving
|
||||
# your associations when the parent is saved. In addition to saving, it
|
||||
# also destroys any associations that were marked for destruction.
|
||||
# (See mark_for_destruction and marked_for_destruction?)
|
||||
#
|
||||
# Saving of the parent, its associations, and the destruction of marked
|
||||
# associations, all happen inside 1 transaction. This should never leave the
|
||||
# database in an inconsistent state after, for instance, mass assigning
|
||||
# attributes and saving them.
|
||||
#
|
||||
# If validations for any of the associations fail, their error messages will
|
||||
# be applied to the parent.
|
||||
#
|
||||
# Note that it also means that associations marked for destruction won't
|
||||
# be destroyed directly. They will however still be marked for destruction.
|
||||
#
|
||||
# === One-to-one Example
|
||||
#
|
||||
# Consider a Post model with one Author:
|
||||
#
|
||||
# class Post
|
||||
# has_one :author, :autosave => true
|
||||
# end
|
||||
#
|
||||
# Saving changes to the parent and its associated model can now be performed
|
||||
# automatically _and_ atomically:
|
||||
#
|
||||
# post = Post.find(1)
|
||||
# post.title # => "The current global position of migrating ducks"
|
||||
# post.author.name # => "alloy"
|
||||
#
|
||||
# post.title = "On the migration of ducks"
|
||||
# post.author.name = "Eloy Duran"
|
||||
#
|
||||
# post.save
|
||||
# post.reload
|
||||
# post.title # => "On the migration of ducks"
|
||||
# post.author.name # => "Eloy Duran"
|
||||
#
|
||||
# Destroying an associated model, as part of the parent's save action, is as
|
||||
# simple as marking it for destruction:
|
||||
#
|
||||
# post.author.mark_for_destruction
|
||||
# post.author.marked_for_destruction? # => true
|
||||
#
|
||||
# Note that the model is _not_ yet removed from the database:
|
||||
# id = post.author.id
|
||||
# Author.find_by_id(id).nil? # => false
|
||||
#
|
||||
# post.save
|
||||
# post.reload.author # => nil
|
||||
#
|
||||
# Now it _is_ removed from the database:
|
||||
# Author.find_by_id(id).nil? # => true
|
||||
#
|
||||
# === One-to-many Example
|
||||
#
|
||||
# Consider a Post model with many Comments:
|
||||
#
|
||||
# class Post
|
||||
# has_many :comments, :autosave => true
|
||||
# end
|
||||
#
|
||||
# Saving changes to the parent and its associated model can now be performed
|
||||
# automatically _and_ atomically:
|
||||
#
|
||||
# post = Post.find(1)
|
||||
# post.title # => "The current global position of migrating ducks"
|
||||
# post.comments.first.body # => "Wow, awesome info thanks!"
|
||||
# post.comments.last.body # => "Actually, your article should be named differently."
|
||||
#
|
||||
# post.title = "On the migration of ducks"
|
||||
# post.comments.last.body = "Actually, your article should be named differently. [UPDATED]: You are right, thanks."
|
||||
#
|
||||
# post.save
|
||||
# post.reload
|
||||
# post.title # => "On the migration of ducks"
|
||||
# post.comments.last.body # => "Actually, your article should be named differently. [UPDATED]: You are right, thanks."
|
||||
#
|
||||
# Destroying one of the associated models members, as part of the parent's
|
||||
# save action, is as simple as marking it for destruction:
|
||||
#
|
||||
# post.comments.last.mark_for_destruction
|
||||
# post.comments.last.marked_for_destruction? # => true
|
||||
# post.comments.length # => 2
|
||||
#
|
||||
# Note that the model is _not_ yet removed from the database:
|
||||
# id = post.comments.last.id
|
||||
# Comment.find_by_id(id).nil? # => false
|
||||
#
|
||||
# post.save
|
||||
# post.reload.comments.length # => 1
|
||||
#
|
||||
# Now it _is_ removed from the database:
|
||||
# Comment.find_by_id(id).nil? # => true
|
||||
#
|
||||
# === Validation
|
||||
#
|
||||
# Validation is performed on the parent as usual, but also on all autosave
|
||||
# enabled associations. If any of the associations fail validation, its
|
||||
# error messages will be applied on the parents errors object and validation
|
||||
# of the parent will fail.
|
||||
#
|
||||
# Consider a Post model with Author which validates the presence of its name
|
||||
# attribute:
|
||||
#
|
||||
# class Post
|
||||
# has_one :author, :autosave => true
|
||||
# end
|
||||
#
|
||||
# class Author
|
||||
# validates_presence_of :name
|
||||
# end
|
||||
#
|
||||
# post = Post.find(1)
|
||||
# post.author.name = ''
|
||||
# post.save # => false
|
||||
# post.errors # => #<ActiveRecord::Errors:0x174498c @errors={"author_name"=>["can't be blank"]}, @base=#<Post ...>>
|
||||
#
|
||||
# No validations will be performed on the associated models when validations
|
||||
# are skipped for the parent:
|
||||
#
|
||||
# post = Post.find(1)
|
||||
# post.author.name = ''
|
||||
# post.save(false) # => true
|
||||
module AutosaveAssociation
|
||||
ASSOCIATION_TYPES = %w{ has_one belongs_to has_many has_and_belongs_to_many }
|
||||
|
||||
def self.included(base)
|
||||
base.class_eval do
|
||||
base.extend(ClassMethods)
|
||||
alias_method_chain :reload, :autosave_associations
|
||||
|
||||
ASSOCIATION_TYPES.each do |type|
|
||||
base.send("valid_keys_for_#{type}_association") << :autosave
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
private
|
||||
|
||||
# def belongs_to(name, options = {})
|
||||
# super
|
||||
# add_autosave_association_callbacks(reflect_on_association(name))
|
||||
# end
|
||||
ASSOCIATION_TYPES.each do |type|
|
||||
module_eval <<-CODE, __FILE__, __LINE__ + 1
|
||||
def #{type}(name, options = {})
|
||||
super
|
||||
add_autosave_association_callbacks(reflect_on_association(name))
|
||||
end
|
||||
CODE
|
||||
end
|
||||
|
||||
# Adds a validate and save callback for the association as specified by
|
||||
# the +reflection+.
|
||||
#
|
||||
# For performance reasons, we don't check whether to validate at runtime,
|
||||
# but instead only define the method and callback when needed. However,
|
||||
# this can change, for instance, when using nested attributes, which is
|
||||
# called _after_ the association has been defined. Since we don't want
|
||||
# the callbacks to get defined multiple times, there are guards that
|
||||
# check if the save or validation methods have already been defined
|
||||
# before actually defining them.
|
||||
def add_autosave_association_callbacks(reflection)
|
||||
save_method = :"autosave_associated_records_for_#{reflection.name}"
|
||||
validation_method = :"validate_associated_records_for_#{reflection.name}"
|
||||
collection = reflection.collection?
|
||||
|
||||
unless method_defined?(save_method)
|
||||
if collection
|
||||
before_save :before_save_collection_association
|
||||
|
||||
define_method(save_method) { save_collection_association(reflection) }
|
||||
# Doesn't use after_save as that would save associations added in after_create/after_update twice
|
||||
after_create save_method
|
||||
after_update save_method
|
||||
else
|
||||
if reflection.macro == :has_one
|
||||
define_method(save_method) { save_has_one_association(reflection) }
|
||||
after_save save_method
|
||||
else
|
||||
define_method(save_method) { save_belongs_to_association(reflection) }
|
||||
before_save save_method
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if reflection.validate? && !method_defined?(validation_method)
|
||||
method = (collection ? :validate_collection_association : :validate_single_association)
|
||||
define_method(validation_method) { send(method, reflection) }
|
||||
validate validation_method
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Reloads the attributes of the object as usual and removes a mark for destruction.
|
||||
def reload_with_autosave_associations(options = nil)
|
||||
@marked_for_destruction = false
|
||||
reload_without_autosave_associations(options)
|
||||
end
|
||||
|
||||
# Marks this record to be destroyed as part of the parents save transaction.
|
||||
# This does _not_ actually destroy the record yet, rather it will be destroyed when <tt>parent.save</tt> is called.
|
||||
#
|
||||
# Only useful if the <tt>:autosave</tt> option on the parent is enabled for this associated model.
|
||||
def mark_for_destruction
|
||||
@marked_for_destruction = true
|
||||
end
|
||||
|
||||
# Returns whether or not this record will be destroyed as part of the parents save transaction.
|
||||
#
|
||||
# Only useful if the <tt>:autosave</tt> option on the parent is enabled for this associated model.
|
||||
def marked_for_destruction?
|
||||
@marked_for_destruction
|
||||
end
|
||||
|
||||
# Returns whether or not this record has been changed in any way (including whether
|
||||
# any of its nested autosave associations are likewise changed)
|
||||
def changed_for_autosave?
|
||||
new_record? || changed? || marked_for_destruction? || nested_records_changed_for_autosave?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Returns the record for an association collection that should be validated
|
||||
# or saved. If +autosave+ is +false+ only new records will be returned,
|
||||
# unless the parent is/was a new record itself.
|
||||
def associated_records_to_validate_or_save(association, new_record, autosave)
|
||||
if new_record
|
||||
association
|
||||
elsif autosave
|
||||
association.target.select { |record| record.changed_for_autosave? }
|
||||
else
|
||||
association.target.select { |record| record.new_record? }
|
||||
end
|
||||
end
|
||||
|
||||
# go through nested autosave associations that are loaded in memory (without loading
|
||||
# any new ones), and return true if is changed for autosave
|
||||
def nested_records_changed_for_autosave?
|
||||
self.class.reflect_on_all_autosave_associations.each do |reflection|
|
||||
if association = association_instance_get(reflection.name)
|
||||
if [:belongs_to, :has_one].include?(reflection.macro)
|
||||
return true if association.target && association.target.changed_for_autosave?
|
||||
else
|
||||
association.target.each {|record| return true if record.changed_for_autosave? }
|
||||
end
|
||||
end
|
||||
end
|
||||
false
|
||||
end
|
||||
|
||||
# Validate the association if <tt>:validate</tt> or <tt>:autosave</tt> is
|
||||
# turned on for the association specified by +reflection+.
|
||||
def validate_single_association(reflection)
|
||||
if (association = association_instance_get(reflection.name)) && !association.target.nil?
|
||||
association_valid?(reflection, association)
|
||||
end
|
||||
end
|
||||
|
||||
# Validate the associated records if <tt>:validate</tt> or
|
||||
# <tt>:autosave</tt> is turned on for the association specified by
|
||||
# +reflection+.
|
||||
def validate_collection_association(reflection)
|
||||
if association = association_instance_get(reflection.name)
|
||||
if records = associated_records_to_validate_or_save(association, new_record?, reflection.options[:autosave])
|
||||
records.each { |record| association_valid?(reflection, record) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Returns whether or not the association is valid and applies any errors to
|
||||
# the parent, <tt>self</tt>, if it wasn't. Skips any <tt>:autosave</tt>
|
||||
# enabled records if they're marked_for_destruction? or destroyed.
|
||||
def association_valid?(reflection, association)
|
||||
return true if association.destroyed? || association.marked_for_destruction?
|
||||
|
||||
unless valid = association.valid?
|
||||
if reflection.options[:autosave]
|
||||
association.errors.each_error do |attribute, error|
|
||||
attribute = "#{reflection.name}.#{attribute}"
|
||||
errors.add(attribute, error.dup) unless errors.on(attribute)
|
||||
end
|
||||
else
|
||||
errors.add(reflection.name)
|
||||
end
|
||||
end
|
||||
valid
|
||||
end
|
||||
|
||||
# Is used as a before_save callback to check while saving a collection
|
||||
# association whether or not the parent was a new record before saving.
|
||||
def before_save_collection_association
|
||||
@new_record_before_save = new_record?
|
||||
true
|
||||
end
|
||||
|
||||
# Saves any new associated records, or all loaded autosave associations if
|
||||
# <tt>:autosave</tt> is enabled on the association.
|
||||
#
|
||||
# In addition, it destroys all children that were marked for destruction
|
||||
# with mark_for_destruction.
|
||||
#
|
||||
# This all happens inside a transaction, _if_ the Transactions module is included into
|
||||
# ActiveRecord::Base after the AutosaveAssociation module, which it does by default.
|
||||
def save_collection_association(reflection)
|
||||
if association = association_instance_get(reflection.name)
|
||||
autosave = reflection.options[:autosave]
|
||||
|
||||
if records = associated_records_to_validate_or_save(association, @new_record_before_save, autosave)
|
||||
records.each do |record|
|
||||
next if record.destroyed?
|
||||
|
||||
if autosave && record.marked_for_destruction?
|
||||
association.destroy(record)
|
||||
elsif autosave != false && (@new_record_before_save || record.new_record?)
|
||||
if autosave
|
||||
saved = association.send(:insert_record, record, false, false)
|
||||
else
|
||||
association.send(:insert_record, record)
|
||||
end
|
||||
elsif autosave
|
||||
saved = record.save(false)
|
||||
end
|
||||
|
||||
raise ActiveRecord::Rollback if saved == false
|
||||
end
|
||||
end
|
||||
|
||||
# reconstruct the SQL queries now that we know the owner's id
|
||||
association.send(:construct_sql) if association.respond_to?(:construct_sql)
|
||||
end
|
||||
end
|
||||
|
||||
# Saves the associated record if it's new or <tt>:autosave</tt> is enabled
|
||||
# on the association.
|
||||
#
|
||||
# In addition, it will destroy the association if it was marked for
|
||||
# destruction with mark_for_destruction.
|
||||
#
|
||||
# This all happens inside a transaction, _if_ the Transactions module is included into
|
||||
# ActiveRecord::Base after the AutosaveAssociation module, which it does by default.
|
||||
def save_has_one_association(reflection)
|
||||
if (association = association_instance_get(reflection.name)) && !association.target.nil? && !association.destroyed?
|
||||
autosave = reflection.options[:autosave]
|
||||
|
||||
if autosave && association.marked_for_destruction?
|
||||
association.destroy
|
||||
else
|
||||
key = reflection.options[:primary_key] ? send(reflection.options[:primary_key]) : id
|
||||
if autosave != false && (new_record? || association.new_record? || association[reflection.primary_key_name] != key || autosave)
|
||||
association[reflection.primary_key_name] = key
|
||||
saved = association.save(!autosave)
|
||||
raise ActiveRecord::Rollback if !saved && autosave
|
||||
saved
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Saves the associated record if it's new or <tt>:autosave</tt> is enabled
|
||||
# on the association.
|
||||
#
|
||||
# In addition, it will destroy the association if it was marked for
|
||||
# destruction with mark_for_destruction.
|
||||
#
|
||||
# This all happens inside a transaction, _if_ the Transactions module is included into
|
||||
# ActiveRecord::Base after the AutosaveAssociation module, which it does by default.
|
||||
def save_belongs_to_association(reflection)
|
||||
if (association = association_instance_get(reflection.name)) && !association.destroyed?
|
||||
autosave = reflection.options[:autosave]
|
||||
|
||||
if autosave && association.marked_for_destruction?
|
||||
association.destroy
|
||||
elsif autosave != false
|
||||
saved = association.save(!autosave) if association.new_record? || autosave
|
||||
|
||||
if association.updated?
|
||||
association_id = association.send(reflection.options[:primary_key] || :id)
|
||||
self[reflection.primary_key_name] = association_id
|
||||
# TODO: Removing this code doesn't seem to matter…
|
||||
if reflection.options[:polymorphic]
|
||||
self[reflection.options[:foreign_type]] = association.class.base_class.name.to_s
|
||||
end
|
||||
end
|
||||
|
||||
saved if autosave
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
3218
vendor/rails/activerecord/lib/active_record/base.rb
vendored
3218
vendor/rails/activerecord/lib/active_record/base.rb
vendored
File diff suppressed because it is too large
Load diff
|
|
@ -1,85 +0,0 @@
|
|||
module ActiveRecord
|
||||
module Batches # :nodoc:
|
||||
def self.included(base)
|
||||
base.extend(ClassMethods)
|
||||
end
|
||||
|
||||
# When processing large numbers of records, it's often a good idea to do
|
||||
# so in batches to prevent memory ballooning.
|
||||
module ClassMethods
|
||||
# Yields each record that was found by the find +options+. The find is
|
||||
# performed by find_in_batches with a batch size of 1000 (or as
|
||||
# specified by the <tt>:batch_size</tt> option).
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# Person.find_each(:conditions => "age > 21") do |person|
|
||||
# person.party_all_night!
|
||||
# end
|
||||
#
|
||||
# Note: This method is only intended to use for batch processing of
|
||||
# large amounts of records that wouldn't fit in memory all at once. If
|
||||
# you just need to loop over less than 1000 records, it's probably
|
||||
# better just to use the regular find methods.
|
||||
def find_each(options = {})
|
||||
find_in_batches(options) do |records|
|
||||
records.each { |record| yield record }
|
||||
end
|
||||
|
||||
self
|
||||
end
|
||||
|
||||
# Yields each batch of records that was found by the find +options+ as
|
||||
# an array. The size of each batch is set by the <tt>:batch_size</tt>
|
||||
# option; the default is 1000.
|
||||
#
|
||||
# You can control the starting point for the batch processing by
|
||||
# supplying the <tt>:start</tt> option. This is especially useful if you
|
||||
# want multiple workers dealing with the same processing queue. You can
|
||||
# make worker 1 handle all the records between id 0 and 10,000 and
|
||||
# worker 2 handle from 10,000 and beyond (by setting the <tt>:start</tt>
|
||||
# option on that worker).
|
||||
#
|
||||
# It's not possible to set the order. That is automatically set to
|
||||
# ascending on the primary key ("id ASC") to make the batch ordering
|
||||
# work. This also mean that this method only works with integer-based
|
||||
# primary keys. You can't set the limit either, that's used to control
|
||||
# the the batch sizes.
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# Person.find_in_batches(:conditions => "age > 21") do |group|
|
||||
# sleep(50) # Make sure it doesn't get too crowded in there!
|
||||
# group.each { |person| person.party_all_night! }
|
||||
# end
|
||||
def find_in_batches(options = {})
|
||||
raise "You can't specify an order, it's forced to be #{batch_order}" if options[:order]
|
||||
raise "You can't specify a limit, it's forced to be the batch_size" if options[:limit]
|
||||
|
||||
start = options.delete(:start).to_i
|
||||
batch_size = options.delete(:batch_size) || 1000
|
||||
|
||||
proxy = scoped(options.merge(:order => batch_order, :limit => batch_size))
|
||||
records = proxy.find(:all, :conditions => [ "#{table_name}.#{primary_key} >= ?", start ])
|
||||
|
||||
while records.any?
|
||||
yield records
|
||||
|
||||
break if records.size < batch_size
|
||||
|
||||
last_value = records.last.id
|
||||
|
||||
raise "You must include the primary key if you define a select" unless last_value.present?
|
||||
|
||||
records = proxy.find(:all, :conditions => [ "#{table_name}.#{primary_key} > ?", last_value ])
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
private
|
||||
def batch_order
|
||||
"#{table_name}.#{primary_key} ASC"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,314 +0,0 @@
|
|||
module ActiveRecord
|
||||
module Calculations #:nodoc:
|
||||
CALCULATIONS_OPTIONS = [:conditions, :joins, :order, :select, :group, :having, :distinct, :limit, :offset, :include, :from]
|
||||
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 to ActiveRecord::Base.
|
||||
# * <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) ...
|
||||
# * <tt>:from</tt> - By default, this is the table name of the class, but can be changed to an alternate table name (or even the name
|
||||
# of a database view).
|
||||
#
|
||||
# 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, or +nil+ if there's no row. See +calculate+ for examples with
|
||||
# options.
|
||||
#
|
||||
# Person.average('age') # => 35.8
|
||||
def average(column_name, options = {})
|
||||
calculate(:avg, column_name, options)
|
||||
end
|
||||
|
||||
# Calculates the minimum value on a given column. The value is returned
|
||||
# with the same data type of the column, or +nil+ if there's no row. See
|
||||
# +calculate+ for examples with options.
|
||||
#
|
||||
# Person.minimum('age') # => 7
|
||||
def minimum(column_name, options = {})
|
||||
calculate(:min, column_name, options)
|
||||
end
|
||||
|
||||
# Calculates the maximum value on a given column. The value is returned
|
||||
# with the same data type of the column, or +nil+ if there's no row. See
|
||||
# +calculate+ for examples with options.
|
||||
#
|
||||
# Person.maximum('age') # => 93
|
||||
def maximum(column_name, options = {})
|
||||
calculate(:max, column_name, options)
|
||||
end
|
||||
|
||||
# Calculates the sum of values on a given column. The value is returned
|
||||
# with the same data type of the column, 0 if there's no row. See
|
||||
# +calculate+ for examples with options.
|
||||
#
|
||||
# Person.sum('age') # => 4562
|
||||
def sum(column_name, options = {})
|
||||
calculate(:sum, column_name, options)
|
||||
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 to ActiveRecord::Base.
|
||||
# * <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]
|
||||
if options[:from]
|
||||
sql << " FROM #{options[:from]} "
|
||||
elsif scope && scope[:from] && !use_workaround
|
||||
sql << " FROM #{scope[:from]} "
|
||||
else
|
||||
sql << " FROM (SELECT #{distinct}#{column_name}" if use_workaround
|
||||
sql << " FROM #{connection.quote_table_name(table_name)} "
|
||||
end
|
||||
|
||||
joins = ""
|
||||
add_joins!(joins, options[:joins], scope)
|
||||
|
||||
if merged_includes.any?
|
||||
join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, merged_includes, joins)
|
||||
sql << join_dependency.join_associations.collect{|join| join.association_join }.join
|
||||
end
|
||||
|
||||
sql << joins unless joins.blank?
|
||||
|
||||
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]
|
||||
having = sanitize_sql_for_conditions(options[:having])
|
||||
|
||||
# FrontBase requires identifiers in the HAVING clause and chokes on function calls
|
||||
if connection.adapter_name == 'FrontBase'
|
||||
having.downcase!
|
||||
having.gsub!(/#{operation}\s*\(\s*#{column_name}\s*\)/, aggregate_alias)
|
||||
end
|
||||
|
||||
sql << " HAVING #{having} "
|
||||
end
|
||||
|
||||
sql << " ORDER BY #{options[:order]} " if options[:order]
|
||||
add_limit!(sql, options, scope)
|
||||
sql << ") #{aggregate_alias}_subquery" 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)
|
||||
table_name = keys.join(' ')
|
||||
table_name.downcase!
|
||||
table_name.gsub!(/\*/, 'all')
|
||||
table_name.gsub!(/\W+/, ' ')
|
||||
table_name.strip!
|
||||
table_name.gsub!(/ +/, '_')
|
||||
|
||||
connection.table_alias_for(table_name)
|
||||
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)
|
||||
if value.is_a?(String) || value.nil?
|
||||
case operation.to_s.downcase
|
||||
when 'count' then value.to_i
|
||||
when 'sum' then type_cast_using_column(value || '0', column)
|
||||
when 'avg' then value.try(:to_d)
|
||||
else type_cast_using_column(value, column)
|
||||
end
|
||||
else
|
||||
value
|
||||
end
|
||||
end
|
||||
|
||||
def type_cast_using_column(value, column)
|
||||
column ? column.type_cast(value) : value
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,360 +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 for a new record:
|
||||
#
|
||||
# * (-) <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. The sequence for calling <tt>Base#save</tt> an existing record is similar, except that each
|
||||
# <tt>_on_create</tt> callback is replaced by the corresponding <tt>_on_update</tt> callback.
|
||||
#
|
||||
# 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 overwritable 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 descendant to decide whether they want to call +super+ and trigger the inherited callbacks.
|
||||
#
|
||||
# *IMPORTANT:* In order for inheritance to work for the callback queues, you must specify the callbacks before specifying the
|
||||
# associations. Otherwise, you might trigger the loading of a child before the parent has registered the callbacks and they won't
|
||||
# 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
|
||||
# after_save EncryptionWrapper.new
|
||||
# after_initialize EncryptionWrapper.new
|
||||
# end
|
||||
#
|
||||
# class EncryptionWrapper
|
||||
# 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. You can make these callbacks more flexible by passing in other
|
||||
# initialization data such as the name of the attribute to work with:
|
||||
#
|
||||
# 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.send("#{@attribute}=", encrypt(record.send("#{@attribute}")))
|
||||
# end
|
||||
#
|
||||
# def after_save(record)
|
||||
# record.send("#{@attribute}=", decrypt(record.send("#{@attribute}")))
|
||||
# end
|
||||
#
|
||||
# alias_method :after_find, :after_save
|
||||
#
|
||||
# private
|
||||
# def encrypt(value)
|
||||
# # Secrecy is committed
|
||||
# end
|
||||
#
|
||||
# def decrypt(value)
|
||||
# # Secrecy is unveiled
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# 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 ActiveRecord::RecordInvalid 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.
|
||||
#
|
||||
# == Transactions
|
||||
#
|
||||
# The entire callback chain of a +save+, <tt>save!</tt>, or +destroy+ call runs
|
||||
# within a transaction. That includes <tt>after_*</tt> hooks. If everything
|
||||
# goes fine a COMMIT is executed once the chain has been completed.
|
||||
#
|
||||
# If a <tt>before_*</tt> callback cancels the action a ROLLBACK is issued. You
|
||||
# can also trigger a ROLLBACK raising an exception in any of the callbacks,
|
||||
# including <tt>after_*</tt> hooks. Note, however, that in that case the client
|
||||
# needs to be aware of it because an ordinary +save+ will raise such exception
|
||||
# instead of quietly returning +false+.
|
||||
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).
|
||||
# Note that this callback is still wrapped in the transaction around +save+. For example, if you
|
||||
# invoke an external indexer at this point it won't see the changes in the database.
|
||||
#
|
||||
# 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
|
||||
if result = create_or_update_without_callbacks
|
||||
callback(:after_save)
|
||||
end
|
||||
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).
|
||||
# Note that this callback is still wrapped in the transaction around +save+. For example, if you
|
||||
# invoke an external indexer at this point it won't see the changes in the database.
|
||||
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.
|
||||
# Note that this callback is still wrapped in the transaction around +save+. For example, if you
|
||||
# invoke an external indexer at this point it won't see the changes in the database.
|
||||
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 false == result
|
||||
|
||||
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)
|
||||
result = run_callbacks(method) { |result, object| false == result }
|
||||
|
||||
if result != false && respond_to_without_attributes?(method)
|
||||
result = send(method)
|
||||
end
|
||||
|
||||
notify(method)
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
def notify(method) #:nodoc:
|
||||
self.class.changed
|
||||
self.class.notify_observers(method, self)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,371 +0,0 @@
|
|||
require 'monitor'
|
||||
require 'set'
|
||||
|
||||
module ActiveRecord
|
||||
# Raised when a connection could not be obtained within the connection
|
||||
# acquisition timeout period.
|
||||
class ConnectionTimeoutError < ConnectionNotEstablished
|
||||
end
|
||||
|
||||
module ConnectionAdapters
|
||||
# Connection pool base class for managing ActiveRecord database
|
||||
# connections.
|
||||
#
|
||||
# == Introduction
|
||||
#
|
||||
# A connection pool synchronizes thread access to a limited number of
|
||||
# database connections. The basic idea is that each thread checks out a
|
||||
# database connection from the pool, uses that connection, and checks the
|
||||
# connection back in. ConnectionPool is completely thread-safe, and will
|
||||
# ensure that a connection cannot be used by two threads at the same time,
|
||||
# as long as ConnectionPool's contract is correctly followed. It will also
|
||||
# handle cases in which there are more threads than connections: if all
|
||||
# connections have been checked out, and a thread tries to checkout a
|
||||
# connection anyway, then ConnectionPool will wait until some other thread
|
||||
# has checked in a connection.
|
||||
#
|
||||
# == Obtaining (checking out) a connection
|
||||
#
|
||||
# Connections can be obtained and used from a connection pool in several
|
||||
# ways:
|
||||
#
|
||||
# 1. Simply use ActiveRecord::Base.connection as with ActiveRecord 2.1 and
|
||||
# earlier (pre-connection-pooling). Eventually, when you're done with
|
||||
# the connection(s) and wish it to be returned to the pool, you call
|
||||
# ActiveRecord::Base.clear_active_connections!. This will be the
|
||||
# default behavior for ActiveRecord when used in conjunction with
|
||||
# ActionPack's request handling cycle.
|
||||
# 2. Manually check out a connection from the pool with
|
||||
# ActiveRecord::Base.connection_pool.checkout. You are responsible for
|
||||
# returning this connection to the pool when finished by calling
|
||||
# ActiveRecord::Base.connection_pool.checkin(connection).
|
||||
# 3. Use ActiveRecord::Base.connection_pool.with_connection(&block), which
|
||||
# obtains a connection, yields it as the sole argument to the block,
|
||||
# and returns it to the pool after the block completes.
|
||||
#
|
||||
# Connections in the pool are actually AbstractAdapter objects (or objects
|
||||
# compatible with AbstractAdapter's interface).
|
||||
#
|
||||
# == Options
|
||||
#
|
||||
# There are two connection-pooling-related options that you can add to
|
||||
# your database connection configuration:
|
||||
#
|
||||
# * +pool+: number indicating size of connection pool (default 5)
|
||||
# * +wait_timeout+: number of seconds to block and wait for a connection
|
||||
# before giving up and raising a timeout error (default 5 seconds).
|
||||
class ConnectionPool
|
||||
attr_reader :spec
|
||||
|
||||
# Creates a new ConnectionPool object. +spec+ is a ConnectionSpecification
|
||||
# object which describes database connection information (e.g. adapter,
|
||||
# host name, username, password, etc), as well as the maximum size for
|
||||
# this ConnectionPool.
|
||||
#
|
||||
# The default ConnectionPool maximum size is 5.
|
||||
def initialize(spec)
|
||||
@spec = spec
|
||||
|
||||
# The cache of reserved connections mapped to threads
|
||||
@reserved_connections = {}
|
||||
|
||||
# The mutex used to synchronize pool access
|
||||
@connection_mutex = Monitor.new
|
||||
@queue = @connection_mutex.new_cond
|
||||
|
||||
# default 5 second timeout unless on ruby 1.9
|
||||
@timeout =
|
||||
if RUBY_VERSION < '1.9'
|
||||
spec.config[:wait_timeout] || 5
|
||||
end
|
||||
|
||||
# default max pool size to 5
|
||||
@size = (spec.config[:pool] && spec.config[:pool].to_i) || 5
|
||||
|
||||
@connections = []
|
||||
@checked_out = []
|
||||
end
|
||||
|
||||
# Retrieve the connection associated with the current thread, or call
|
||||
# #checkout to obtain one if necessary.
|
||||
#
|
||||
# #connection can be called any number of times; the connection is
|
||||
# held in a hash keyed by the thread id.
|
||||
def connection
|
||||
if conn = @reserved_connections[current_connection_id]
|
||||
conn
|
||||
else
|
||||
@reserved_connections[current_connection_id] = checkout
|
||||
end
|
||||
end
|
||||
|
||||
# Signal that the thread is finished with the current connection.
|
||||
# #release_connection releases the connection-thread association
|
||||
# and returns the connection to the pool.
|
||||
def release_connection
|
||||
conn = @reserved_connections.delete(current_connection_id)
|
||||
checkin conn if conn
|
||||
end
|
||||
|
||||
# Reserve a connection, and yield it to a block. Ensure the connection is
|
||||
# checked back in when finished.
|
||||
def with_connection
|
||||
conn = checkout
|
||||
yield conn
|
||||
ensure
|
||||
checkin conn
|
||||
end
|
||||
|
||||
# Returns true if a connection has already been opened.
|
||||
def connected?
|
||||
!@connections.empty?
|
||||
end
|
||||
|
||||
# Disconnects all connections in the pool, and clears the pool.
|
||||
def disconnect!
|
||||
@reserved_connections.each do |name,conn|
|
||||
checkin conn
|
||||
end
|
||||
@reserved_connections = {}
|
||||
@connections.each do |conn|
|
||||
conn.disconnect!
|
||||
end
|
||||
@connections = []
|
||||
end
|
||||
|
||||
# Clears the cache which maps classes
|
||||
def clear_reloadable_connections!
|
||||
@reserved_connections.each do |name, conn|
|
||||
checkin conn
|
||||
end
|
||||
@reserved_connections = {}
|
||||
@connections.each do |conn|
|
||||
conn.disconnect! if conn.requires_reloading?
|
||||
end
|
||||
@connections = []
|
||||
end
|
||||
|
||||
# Verify active connections and remove and disconnect connections
|
||||
# associated with stale threads.
|
||||
def verify_active_connections! #:nodoc:
|
||||
clear_stale_cached_connections!
|
||||
@connections.each do |connection|
|
||||
connection.verify!
|
||||
end
|
||||
end
|
||||
|
||||
# Return any checked-out connections back to the pool by threads that
|
||||
# are no longer alive.
|
||||
def clear_stale_cached_connections!
|
||||
remove_stale_cached_threads!(@reserved_connections) do |name, conn|
|
||||
checkin conn
|
||||
end
|
||||
end
|
||||
|
||||
# Check-out a database connection from the pool, indicating that you want
|
||||
# to use it. You should call #checkin when you no longer need this.
|
||||
#
|
||||
# This is done by either returning an existing connection, or by creating
|
||||
# a new connection. If the maximum number of connections for this pool has
|
||||
# already been reached, but the pool is empty (i.e. they're all being used),
|
||||
# then this method will wait until a thread has checked in a connection.
|
||||
# The wait time is bounded however: if no connection can be checked out
|
||||
# within the timeout specified for this pool, then a ConnectionTimeoutError
|
||||
# exception will be raised.
|
||||
#
|
||||
# Returns: an AbstractAdapter object.
|
||||
#
|
||||
# Raises:
|
||||
# - ConnectionTimeoutError: no connection can be obtained from the pool
|
||||
# within the timeout period.
|
||||
def checkout
|
||||
# Checkout an available connection
|
||||
@connection_mutex.synchronize do
|
||||
loop do
|
||||
conn = if @checked_out.size < @connections.size
|
||||
checkout_existing_connection
|
||||
elsif @connections.size < @size
|
||||
checkout_new_connection
|
||||
end
|
||||
return conn if conn
|
||||
# No connections available; wait for one
|
||||
if @queue.wait(@timeout)
|
||||
next
|
||||
else
|
||||
# try looting dead threads
|
||||
clear_stale_cached_connections!
|
||||
if @size == @checked_out.size
|
||||
raise ConnectionTimeoutError, "could not obtain a database connection#{" within #{@timeout} seconds" if @timeout}. The max pool size is currently #{@size}; consider increasing it."
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Check-in a database connection back into the pool, indicating that you
|
||||
# no longer need this connection.
|
||||
#
|
||||
# +conn+: an AbstractAdapter object, which was obtained by earlier by
|
||||
# calling +checkout+ on this pool.
|
||||
def checkin(conn)
|
||||
@connection_mutex.synchronize do
|
||||
conn.run_callbacks :checkin
|
||||
@checked_out.delete conn
|
||||
@queue.signal
|
||||
end
|
||||
end
|
||||
|
||||
synchronize :clear_reloadable_connections!, :verify_active_connections!,
|
||||
:connected?, :disconnect!, :with => :@connection_mutex
|
||||
|
||||
private
|
||||
def new_connection
|
||||
ActiveRecord::Base.send(spec.adapter_method, spec.config)
|
||||
end
|
||||
|
||||
def current_connection_id #:nodoc:
|
||||
Thread.current.object_id
|
||||
end
|
||||
|
||||
# Remove stale threads from the cache.
|
||||
def remove_stale_cached_threads!(cache, &block)
|
||||
keys = Set.new(cache.keys)
|
||||
|
||||
Thread.list.each do |thread|
|
||||
keys.delete(thread.object_id) if thread.alive?
|
||||
end
|
||||
keys.each do |key|
|
||||
next unless cache.has_key?(key)
|
||||
block.call(key, cache[key])
|
||||
cache.delete(key)
|
||||
end
|
||||
end
|
||||
|
||||
def checkout_new_connection
|
||||
c = new_connection
|
||||
@connections << c
|
||||
checkout_and_verify(c)
|
||||
end
|
||||
|
||||
def checkout_existing_connection
|
||||
c = (@connections - @checked_out).first
|
||||
checkout_and_verify(c)
|
||||
end
|
||||
|
||||
def checkout_and_verify(c)
|
||||
c.verify!
|
||||
c.run_callbacks :checkout
|
||||
@checked_out << c
|
||||
c
|
||||
end
|
||||
end
|
||||
|
||||
# ConnectionHandler is a collection of ConnectionPool objects. It is used
|
||||
# for keeping separate connection pools for ActiveRecord models that connect
|
||||
# to different databases.
|
||||
#
|
||||
# For example, suppose that you have 5 models, with the following hierarchy:
|
||||
#
|
||||
# |
|
||||
# +-- Book
|
||||
# | |
|
||||
# | +-- ScaryBook
|
||||
# | +-- GoodBook
|
||||
# +-- Author
|
||||
# +-- BankAccount
|
||||
#
|
||||
# Suppose that Book is to connect to a separate database (i.e. one other
|
||||
# than the default database). Then Book, ScaryBook and GoodBook will all use
|
||||
# the same connection pool. Likewise, Author and BankAccount will use the
|
||||
# same connection pool. However, the connection pool used by Author/BankAccount
|
||||
# is not the same as the one used by Book/ScaryBook/GoodBook.
|
||||
#
|
||||
# Normally there is only a single ConnectionHandler instance, accessible via
|
||||
# ActiveRecord::Base.connection_handler. ActiveRecord models use this to
|
||||
# determine that connection pool that they should use.
|
||||
class ConnectionHandler
|
||||
def initialize(pools = {})
|
||||
@connection_pools = pools
|
||||
end
|
||||
|
||||
def connection_pools
|
||||
@connection_pools ||= {}
|
||||
end
|
||||
|
||||
def establish_connection(name, spec)
|
||||
@connection_pools[name] = ConnectionAdapters::ConnectionPool.new(spec)
|
||||
end
|
||||
|
||||
# Returns any connections in use by the current thread back to the pool,
|
||||
# and also returns connections to the pool cached by threads that are no
|
||||
# longer alive.
|
||||
def clear_active_connections!
|
||||
@connection_pools.each_value {|pool| pool.release_connection }
|
||||
end
|
||||
|
||||
# Clears the cache which maps classes
|
||||
def clear_reloadable_connections!
|
||||
@connection_pools.each_value {|pool| pool.clear_reloadable_connections! }
|
||||
end
|
||||
|
||||
def clear_all_connections!
|
||||
@connection_pools.each_value {|pool| pool.disconnect! }
|
||||
end
|
||||
|
||||
# Verify active connections.
|
||||
def verify_active_connections! #:nodoc:
|
||||
@connection_pools.each_value {|pool| pool.verify_active_connections! }
|
||||
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 retrieve_connection(klass) #:nodoc:
|
||||
pool = retrieve_connection_pool(klass)
|
||||
(pool && pool.connection) or raise ConnectionNotEstablished
|
||||
end
|
||||
|
||||
# Returns true if a connection that's accessible to this class has
|
||||
# already been opened.
|
||||
def connected?(klass)
|
||||
conn = retrieve_connection_pool(klass)
|
||||
conn ? conn.connected? : 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 remove_connection(klass)
|
||||
pool = @connection_pools[klass.name]
|
||||
@connection_pools.delete_if { |key, value| value == pool }
|
||||
pool.disconnect! if pool
|
||||
pool.spec.config if pool
|
||||
end
|
||||
|
||||
def retrieve_connection_pool(klass)
|
||||
pool = @connection_pools[klass.name]
|
||||
return pool if pool
|
||||
return nil if ActiveRecord::Base == klass
|
||||
retrieve_connection_pool klass.superclass
|
||||
end
|
||||
end
|
||||
|
||||
class ConnectionManagement
|
||||
def initialize(app)
|
||||
@app = app
|
||||
end
|
||||
|
||||
def call(env)
|
||||
@app.call(env)
|
||||
ensure
|
||||
# Don't return connection (and peform implicit rollback) if
|
||||
# this request is a part of integration test
|
||||
unless env.key?("rack.test")
|
||||
ActiveRecord::Base.clear_active_connections!
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,139 +0,0 @@
|
|||
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
|
||||
|
||||
##
|
||||
# :singleton-method:
|
||||
# The connection handler
|
||||
class_attribute :connection_handler
|
||||
self.connection_handler = ConnectionAdapters::ConnectionHandler.new
|
||||
|
||||
# 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
|
||||
self.connection_handler.establish_connection(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
|
||||
|
||||
class << self
|
||||
# Deprecated and no longer has any effect.
|
||||
def allow_concurrency
|
||||
ActiveSupport::Deprecation.warn("ActiveRecord::Base.allow_concurrency has been deprecated and no longer has any effect. Please remove all references to allow_concurrency.")
|
||||
end
|
||||
|
||||
# Deprecated and no longer has any effect.
|
||||
def allow_concurrency=(flag)
|
||||
ActiveSupport::Deprecation.warn("ActiveRecord::Base.allow_concurrency= has been deprecated and no longer has any effect. Please remove all references to allow_concurrency=.")
|
||||
end
|
||||
|
||||
# Deprecated and no longer has any effect.
|
||||
def verification_timeout
|
||||
ActiveSupport::Deprecation.warn("ActiveRecord::Base.verification_timeout has been deprecated and no longer has any effect. Please remove all references to verification_timeout.")
|
||||
end
|
||||
|
||||
# Deprecated and no longer has any effect.
|
||||
def verification_timeout=(flag)
|
||||
ActiveSupport::Deprecation.warn("ActiveRecord::Base.verification_timeout= has been deprecated and no longer has any effect. Please remove all references to verification_timeout=.")
|
||||
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
|
||||
retrieve_connection
|
||||
end
|
||||
|
||||
def connection_pool
|
||||
connection_handler.retrieve_connection_pool(self)
|
||||
end
|
||||
|
||||
def retrieve_connection
|
||||
connection_handler.retrieve_connection(self)
|
||||
end
|
||||
|
||||
# Returns true if +ActiveRecord+ is connected.
|
||||
def connected?
|
||||
connection_handler.connected?(self)
|
||||
end
|
||||
|
||||
def remove_connection(klass = self)
|
||||
connection_handler.remove_connection(klass)
|
||||
end
|
||||
|
||||
delegate :clear_active_connections!, :clear_reloadable_connections!,
|
||||
:clear_all_connections!,:verify_active_connections!, :to => :connection_handler
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
module ActiveRecord
|
||||
module ConnectionAdapters # :nodoc:
|
||||
module DatabaseLimits
|
||||
|
||||
# the maximum length of a table alias
|
||||
def table_alias_length
|
||||
255
|
||||
end
|
||||
|
||||
# the maximum length of a column name
|
||||
def column_name_length
|
||||
64
|
||||
end
|
||||
|
||||
# the maximum length of a table name
|
||||
def table_name_length
|
||||
64
|
||||
end
|
||||
|
||||
# the maximum length of an index name
|
||||
def index_name_length
|
||||
64
|
||||
end
|
||||
|
||||
# the maximum number of columns per table
|
||||
def columns_per_table
|
||||
1024
|
||||
end
|
||||
|
||||
# the maximum number of indexes per table
|
||||
def indexes_per_table
|
||||
16
|
||||
end
|
||||
|
||||
# the maximum number of columns in a multicolumn index
|
||||
def columns_per_multicolumn_index
|
||||
16
|
||||
end
|
||||
|
||||
# the maximum number of elements in an IN (x,y,z) clause
|
||||
def in_clause_length
|
||||
65535
|
||||
end
|
||||
|
||||
# the maximum length of a SQL query
|
||||
def sql_query_length
|
||||
1048575
|
||||
end
|
||||
|
||||
# maximum number of joins in a single query
|
||||
def joins_per_query
|
||||
256
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,289 +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)
|
||||
end
|
||||
undef_method :select_rows
|
||||
|
||||
# Executes the SQL statement in the context of this connection.
|
||||
def execute(sql, name = nil, skip_logging = false)
|
||||
end
|
||||
undef_method :execute
|
||||
|
||||
# Returns the last auto-generated ID from the affected table.
|
||||
def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
|
||||
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
|
||||
|
||||
# Checks whether there is currently no transaction active. This is done
|
||||
# by querying the database driver, and does not use the transaction
|
||||
# house-keeping information recorded by #increment_open_transactions and
|
||||
# friends.
|
||||
#
|
||||
# Returns true if there is no transaction active, false if there is a
|
||||
# transaction active, and nil if this information is unknown.
|
||||
#
|
||||
# Not all adapters supports transaction state introspection. Currently,
|
||||
# only the PostgreSQL adapter supports this.
|
||||
def outside_transaction?
|
||||
nil
|
||||
end
|
||||
|
||||
# Runs the given block in a database transaction, and returns the result
|
||||
# of the block.
|
||||
#
|
||||
# == Nested transactions support
|
||||
#
|
||||
# Most databases don't support true nested transactions. At the time of
|
||||
# writing, the only database that supports true nested transactions that
|
||||
# we're aware of, is MS-SQL.
|
||||
#
|
||||
# In order to get around this problem, #transaction will emulate the effect
|
||||
# of nested transactions, by using savepoints:
|
||||
# http://dev.mysql.com/doc/refman/5.0/en/savepoints.html
|
||||
# Savepoints are supported by MySQL and PostgreSQL, but not SQLite3.
|
||||
#
|
||||
# It is safe to call this method if a database transaction is already open,
|
||||
# i.e. if #transaction is called within another #transaction block. In case
|
||||
# of a nested call, #transaction will behave as follows:
|
||||
#
|
||||
# - The block will be run without doing anything. All database statements
|
||||
# that happen within the block are effectively appended to the already
|
||||
# open database transaction.
|
||||
# - However, if +:requires_new+ is set, the block will be wrapped in a
|
||||
# database savepoint acting as a sub-transaction.
|
||||
#
|
||||
# === Caveats
|
||||
#
|
||||
# MySQL doesn't support DDL transactions. If you perform a DDL operation,
|
||||
# then any created savepoints will be automatically released. For example,
|
||||
# if you've created a savepoint, then you execute a CREATE TABLE statement,
|
||||
# then the savepoint that was created will be automatically released.
|
||||
#
|
||||
# This means that, on MySQL, you shouldn't execute DDL operations inside
|
||||
# a #transaction call that you know might create a savepoint. Otherwise,
|
||||
# #transaction will raise exceptions when it tries to release the
|
||||
# already-automatically-released savepoints:
|
||||
#
|
||||
# Model.connection.transaction do # BEGIN
|
||||
# Model.connection.transaction(:requires_new => true) do # CREATE SAVEPOINT active_record_1
|
||||
# Model.connection.create_table(...)
|
||||
# # active_record_1 now automatically released
|
||||
# end # RELEASE SAVEPOINT active_record_1 <--- BOOM! database error!
|
||||
# end
|
||||
def transaction(options = {})
|
||||
options.assert_valid_keys :requires_new, :joinable
|
||||
|
||||
last_transaction_joinable = @transaction_joinable
|
||||
if options.has_key?(:joinable)
|
||||
@transaction_joinable = options[:joinable]
|
||||
else
|
||||
@transaction_joinable = true
|
||||
end
|
||||
requires_new = options[:requires_new] || !last_transaction_joinable
|
||||
|
||||
transaction_open = false
|
||||
begin
|
||||
if block_given?
|
||||
if requires_new || open_transactions == 0
|
||||
if open_transactions == 0
|
||||
begin_db_transaction
|
||||
elsif requires_new
|
||||
create_savepoint
|
||||
end
|
||||
increment_open_transactions
|
||||
transaction_open = true
|
||||
end
|
||||
yield
|
||||
end
|
||||
rescue Exception => database_transaction_rollback
|
||||
if transaction_open && !outside_transaction?
|
||||
transaction_open = false
|
||||
decrement_open_transactions
|
||||
if open_transactions == 0
|
||||
rollback_db_transaction
|
||||
else
|
||||
rollback_to_savepoint
|
||||
end
|
||||
end
|
||||
raise unless database_transaction_rollback.is_a?(ActiveRecord::Rollback)
|
||||
end
|
||||
ensure
|
||||
@transaction_joinable = last_transaction_joinable
|
||||
|
||||
if outside_transaction?
|
||||
@open_transactions = 0
|
||||
elsif transaction_open
|
||||
decrement_open_transactions
|
||||
begin
|
||||
if open_transactions == 0
|
||||
commit_db_transaction
|
||||
else
|
||||
release_savepoint
|
||||
end
|
||||
rescue Exception => database_transaction_rollback
|
||||
if open_transactions == 0
|
||||
rollback_db_transaction
|
||||
else
|
||||
rollback_to_savepoint
|
||||
end
|
||||
raise
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Begins the transaction (and turns off auto-committing).
|
||||
def begin_db_transaction() end
|
||||
|
||||
# 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, or some SQL
|
||||
# fragment that has the same semantics as LIMIT and OFFSET.
|
||||
#
|
||||
# +options+ must be a Hash which contains a +:limit+ option (required)
|
||||
# and an +:offset+ option (optional).
|
||||
#
|
||||
# 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
|
||||
|
||||
# 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
|
||||
|
||||
def case_sensitive_equality_operator
|
||||
"="
|
||||
end
|
||||
|
||||
def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key)
|
||||
"WHERE #{quoted_primary_key} IN (SELECT #{quoted_primary_key} FROM #{quoted_table_name} #{where_sql})"
|
||||
end
|
||||
|
||||
protected
|
||||
# Returns an array of record hashes with the column names as keys and
|
||||
# column values as values.
|
||||
def select(sql, name = nil)
|
||||
end
|
||||
undef_method :select
|
||||
|
||||
# Returns the last auto-generated ID from the affected table.
|
||||
def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
|
||||
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
|
||||
|
||||
# Sanitizes the given LIMIT parameter in order to prevent SQL injection.
|
||||
#
|
||||
# +limit+ may be anything that can evaluate to a string via #to_s. It
|
||||
# should look like an integer, or a comma-delimited list of integers.
|
||||
#
|
||||
# Returns the sanitized limit parameter, either as an integer, or as a
|
||||
# string which contains a comma-delimited list of integers.
|
||||
def sanitize_limit(limit)
|
||||
if limit.to_s =~ /,/
|
||||
limit.to_s.split(',').map{ |i| i.to_i }.join(',')
|
||||
else
|
||||
limit.to_i
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,94 +0,0 @@
|
|||
module ActiveRecord
|
||||
module ConnectionAdapters # :nodoc:
|
||||
module QueryCache
|
||||
class << self
|
||||
def included(base)
|
||||
base.class_eval do
|
||||
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__ + 1
|
||||
def #{method_name}_with_query_dirty(*args) # def update_with_query_dirty(*args)
|
||||
clear_query_cache if @query_cache_enabled # clear_query_cache if @query_cache_enabled
|
||||
#{method_name}_without_query_dirty(*args) # update_without_query_dirty(*args)
|
||||
end # end
|
||||
#
|
||||
alias_method_chain :#{method_name}, :query_dirty # alias_method_chain :update, :query_dirty
|
||||
end_code
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
attr_reader :query_cache, :query_cache_enabled
|
||||
|
||||
# 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,65 +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)
|
||||
"'#{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
|
||||
"'#{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
|
||||
"'#{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
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,722 +0,0 @@
|
|||
require 'date'
|
||||
require 'set'
|
||||
require 'bigdecimal'
|
||||
require 'bigdecimal/util'
|
||||
|
||||
module ActiveRecord
|
||||
module ConnectionAdapters #:nodoc:
|
||||
# An abstract definition of a column in a table.
|
||||
class Column
|
||||
TRUE_VALUES = [true, 1, '1', 't', 'T', 'true', 'TRUE'].to_set
|
||||
FALSE_VALUES = [false, 0, '0', 'f', 'F', 'false', 'FALSE'].to_set
|
||||
|
||||
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
|
||||
|
||||
# Returns +true+ if the column is either of type string or text.
|
||||
def text?
|
||||
type == :string || type == :text
|
||||
end
|
||||
|
||||
# Returns +true+ if the column is either of type integer, float or decimal.
|
||||
def number?
|
||||
type == :integer || type == :float || type == :decimal
|
||||
end
|
||||
|
||||
def has_default?
|
||||
!default.nil?
|
||||
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.is_a?(String) && value.blank?
|
||||
nil
|
||||
else
|
||||
TRUE_VALUES.include?(value)
|
||||
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, :lengths) #:nodoc:
|
||||
end
|
||||
|
||||
# Abstract representation of a column definition. Instances of this type
|
||||
# are typically created by methods in TableDefinition, and added to the
|
||||
# +columns+ attribute of said TableDefinition object, in order to be used
|
||||
# for generating a number of table creation or table changing SQL statements.
|
||||
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}"
|
||||
column_options = {}
|
||||
column_options[:null] = null unless null.nil?
|
||||
column_options[:default] = default unless default.nil?
|
||||
add_column_options!(column_sql, column_options) unless type.to_sym == :primary_key
|
||||
column_sql
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def add_column_options!(sql, options)
|
||||
base.add_column_options!(sql, options.merge(:column => self))
|
||||
end
|
||||
end
|
||||
|
||||
# Represents the schema of an SQL table in an abstract way. This class
|
||||
# provides methods for manipulating the schema representation.
|
||||
#
|
||||
# Inside migration files, the +t+ object in +create_table+ and
|
||||
# +change_table+ is actually of this type:
|
||||
#
|
||||
# class SomeMigration < ActiveRecord::Migration
|
||||
# def self.up
|
||||
# create_table :foo do |t|
|
||||
# puts t.class # => "ActiveRecord::ConnectionAdapters::TableDefinition"
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# def self.down
|
||||
# ...
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# The table definitions
|
||||
# The Columns are stored as a ColumnDefinition in the +columns+ attribute.
|
||||
class TableDefinition
|
||||
# An array of ColumnDefinition objects, representing the column changes
|
||||
# that have been defined.
|
||||
attr_accessor :columns
|
||||
|
||||
def initialize(base)
|
||||
@columns = []
|
||||
@base = base
|
||||
end
|
||||
|
||||
#Handles non supported datatypes - e.g. XML
|
||||
def method_missing(symbol, *args)
|
||||
if symbol.to_s == 'xml'
|
||||
xml_column_fallback(args)
|
||||
end
|
||||
end
|
||||
|
||||
def xml_column_fallback(*args)
|
||||
case @base.adapter_name.downcase
|
||||
when 'sqlite', 'mysql'
|
||||
options = args.extract_options!
|
||||
column(args[0], :text, options)
|
||||
end
|
||||
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. This is number of characters for <tt>:string</tt> and <tt>:text</tt> columns and number of bytes for :binary and :integer columns.
|
||||
# * <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.
|
||||
#
|
||||
# For clarity's sake: the precision is the number of significant digits,
|
||||
# while the scale is the number of digits that can be stored following
|
||||
# the decimal point. For example, the number 123.45 has a precision of 5
|
||||
# and a scale of 2. A decimal with a precision of 5 and a scale of 2 can
|
||||
# range from -999.99 to 999.99.
|
||||
#
|
||||
# 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)
|
||||
#
|
||||
# # Defines a column with a database-specific type.
|
||||
# td.column(:foo, 'polygon')
|
||||
# # => foo polygon
|
||||
#
|
||||
# == 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) # def string(*args)
|
||||
options = args.extract_options! # options = args.extract_options!
|
||||
column_names = args # column_names = args
|
||||
#
|
||||
column_names.each { |name| column(name, '#{column_type}', options) } # column_names.each { |name| column(name, 'string', options) }
|
||||
end # end
|
||||
EOV
|
||||
end
|
||||
|
||||
# Appends <tt>:datetime</tt> columns <tt>:created_at</tt> and
|
||||
# <tt>:updated_at</tt> to the table.
|
||||
def timestamps(*args)
|
||||
options = args.extract_options!
|
||||
column(:created_at, :datetime, options)
|
||||
column(:updated_at, :datetime, options)
|
||||
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.map(&:to_sql) * ', '
|
||||
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) # def string(*args)
|
||||
options = args.extract_options! # options = args.extract_options!
|
||||
column_names = args # column_names = args
|
||||
#
|
||||
column_names.each do |name| # column_names.each do |name|
|
||||
column = ColumnDefinition.new(@base, name, '#{column_type}') # column = ColumnDefinition.new(@base, name, 'string')
|
||||
if options[:limit] # if options[:limit]
|
||||
column.limit = options[:limit] # column.limit = options[:limit]
|
||||
elsif native['#{column_type}'.to_sym].is_a?(Hash) # elsif native['string'.to_sym].is_a?(Hash)
|
||||
column.limit = native['#{column_type}'.to_sym][:limit] # column.limit = native['string'.to_sym][:limit]
|
||||
end # end
|
||||
column.precision = options[:precision] # column.precision = options[:precision]
|
||||
column.scale = options[:scale] # column.scale = options[:scale]
|
||||
column.default = options[:default] # column.default = options[:default]
|
||||
column.null = options[:null] # column.null = options[:null]
|
||||
@base.add_column(@table_name, name, column.sql_type, options) # @base.add_column(@table_name, name, column.sql_type, options)
|
||||
end # end
|
||||
end # end
|
||||
EOV
|
||||
end
|
||||
|
||||
private
|
||||
def native
|
||||
@base.native_database_types
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -1,489 +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
|
||||
|
||||
# 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 with the name +table_name+. +table_name+ may either
|
||||
# be a String or a Symbol.
|
||||
#
|
||||
# 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() passes a TableDefinition object to the block.
|
||||
# # This form will not only create the table, but also columns for the
|
||||
# # table.
|
||||
# create_table(:suppliers) do |t|
|
||||
# t.column :name, :string, :limit => 60
|
||||
# # Other fields here
|
||||
# end
|
||||
#
|
||||
# === Regular form
|
||||
# # Creates a table called 'suppliers' with no columns.
|
||||
# create_table(:suppliers)
|
||||
# # Add a column to '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.to_s.singularize)) unless options[:id] == false
|
||||
|
||||
yield table_definition if block_given?
|
||||
|
||||
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)
|
||||
raise ArgumentError.new("You must specify at least one column name. Example: remove_column(:people, :first_name)") if column_names.empty?
|
||||
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)
|
||||
#
|
||||
# ====== Creating an index with specific key length
|
||||
# add_index(:accounts, :name, :name => 'by_name', :length => 10)
|
||||
# generates
|
||||
# CREATE INDEX by_name ON accounts(name(10))
|
||||
#
|
||||
# add_index(:accounts, [:name, :surname], :name => 'by_name_surname', :length => {:name => 10, :surname => 15})
|
||||
# generates
|
||||
# CREATE INDEX by_name_surname ON accounts(name(10), surname(15))
|
||||
#
|
||||
# Note: SQLite doesn't support index length
|
||||
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
|
||||
|
||||
if index_name.length > index_name_length
|
||||
@logger.warn("Index name '#{index_name}' on table '#{table_name}' is too long; the limit is #{index_name_length} characters. Skipping.")
|
||||
return
|
||||
end
|
||||
if index_exists?(table_name, index_name, false)
|
||||
@logger.warn("Index name '#{index_name}' on table '#{table_name}' already exists. Skipping.")
|
||||
return
|
||||
end
|
||||
quoted_column_names = quoted_columns_for_index(column_names, options).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 = {})
|
||||
index_name = index_name(table_name, options)
|
||||
unless index_exists?(table_name, index_name, true)
|
||||
@logger.warn("Index name '#{index_name}' on table '#{table_name}' does not exist. Skipping.")
|
||||
return
|
||||
end
|
||||
remove_index!(table_name, index_name)
|
||||
end
|
||||
|
||||
def remove_index!(table_name, index_name) #:nodoc:
|
||||
execute "DROP INDEX #{quote_column_name(index_name)} ON #{table_name}"
|
||||
end
|
||||
|
||||
# Rename an index.
|
||||
#
|
||||
# Rename the index_people_on_last_name index to index_users_on_last_name
|
||||
# rename_index :people, 'index_people_on_last_name', 'index_users_on_last_name'
|
||||
def rename_index(table_name, old_name, new_name)
|
||||
# this is a naive implementation; some DBs may support this more efficiently (Postgres, for instance)
|
||||
old_index_def = indexes(table_name).detect { |i| i.name == old_name }
|
||||
return unless old_index_def
|
||||
remove_index(table_name, :name => old_name)
|
||||
add_index(table_name, old_index_def.columns, :name => new_name, :unique => old_index_def.unique)
|
||||
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
|
||||
|
||||
# Verify the existence of an index.
|
||||
#
|
||||
# The default argument is returned if the underlying implementation does not define the indexes method,
|
||||
# as there's no way to determine the correct answer in that case.
|
||||
def index_exists?(table_name, index_name, default)
|
||||
return default unless respond_to?(:indexes)
|
||||
indexes(table_name).detect { |i| i.name == index_name }
|
||||
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 => "#{Base.table_name_prefix}unique_schema_migrations#{Base.table_name_suffix}"
|
||||
|
||||
# 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, migrations_path = ActiveRecord::Migrator.migrations_path)
|
||||
version = version.to_i
|
||||
sm_table = quote_table_name(ActiveRecord::Migrator.schema_migrations_table_name)
|
||||
|
||||
migrated = select_values("SELECT version FROM #{sm_table}").map(&:to_i)
|
||||
versions = Dir["#{migrations_path}/[0-9]*_*.rb"].map do |filename|
|
||||
filename.split('/').last.split('_').first.to_i
|
||||
end
|
||||
|
||||
unless migrated.include?(version)
|
||||
execute "INSERT INTO #{sm_table} (version) VALUES ('#{version}')"
|
||||
end
|
||||
|
||||
inserted = Set.new
|
||||
(versions - migrated).each do |v|
|
||||
if inserted.include?(v)
|
||||
raise "Duplicate migration #{v}. Please renumber your migrations to resolve the conflict."
|
||||
elsif v < version
|
||||
execute "INSERT INTO #{sm_table} (version) VALUES ('#{v}')"
|
||||
inserted << v
|
||||
end
|
||||
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).dup
|
||||
|
||||
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 (type != :primary_key) && (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 explicitly check for :null to allow change_column to work on migrations
|
||||
if options[:null] == false
|
||||
sql << " NOT NULL"
|
||||
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
|
||||
# Overridden by the mysql adapter for supporting index lengths
|
||||
def quoted_columns_for_index(column_names, options = {})
|
||||
column_names.map {|name| quote_column_name(name) }
|
||||
end
|
||||
|
||||
def options_include_default?(options)
|
||||
options.include?(:default) && !(options[:null] == false && options[:default].nil?)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,249 +0,0 @@
|
|||
require 'benchmark'
|
||||
require 'date'
|
||||
require 'bigdecimal'
|
||||
require 'bigdecimal/util'
|
||||
|
||||
# TODO: Autoload these files
|
||||
require 'active_record/connection_adapters/abstract/schema_definitions'
|
||||
require 'active_record/connection_adapters/abstract/schema_statements'
|
||||
require 'active_record/connection_adapters/abstract/database_statements'
|
||||
require 'active_record/connection_adapters/abstract/quoting'
|
||||
require 'active_record/connection_adapters/abstract/connection_pool'
|
||||
require 'active_record/connection_adapters/abstract/connection_specification'
|
||||
require 'active_record/connection_adapters/abstract/query_cache'
|
||||
require 'active_record/connection_adapters/abstract/database_limits'
|
||||
|
||||
module ActiveRecord
|
||||
module ConnectionAdapters # :nodoc:
|
||||
# ActiveRecord supports multiple database systems. AbstractAdapter and
|
||||
# related classes form the abstraction layer which makes this possible.
|
||||
# An AbstractAdapter represents a connection to a database, and provides an
|
||||
# abstract interface for database-specific functionality such as establishing
|
||||
# a connection, escaping values, building the right SQL fragments for ':offset'
|
||||
# and ':limit' options, etc.
|
||||
#
|
||||
# All the concrete database adapters follow the interface laid down in this class.
|
||||
# ActiveRecord::Base.connection returns an AbstractAdapter object, which
|
||||
# you can use.
|
||||
#
|
||||
# Most of the methods in the adapter are useful during migrations. Most
|
||||
# notably, the instance methods provided by SchemaStatement are very useful.
|
||||
class AbstractAdapter
|
||||
include Quoting, DatabaseStatements, SchemaStatements
|
||||
include DatabaseLimits
|
||||
include QueryCache
|
||||
include ActiveSupport::Callbacks
|
||||
define_callbacks :checkout, :checkin
|
||||
|
||||
@@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
|
||||
|
||||
# Can this adapter determine the primary key for tables not attached
|
||||
# to an ActiveRecord class, such as join tables? Backend specific, as
|
||||
# the abstract adapter always returns +false+.
|
||||
def supports_primary_key?
|
||||
false
|
||||
end
|
||||
|
||||
# Does this adapter support using DISTINCT within COUNT? This is +true+
|
||||
# for all adapters except sqlite.
|
||||
def supports_count_distinct?
|
||||
true
|
||||
end
|
||||
|
||||
# Does this adapter support DDL rollbacks in transactions? That is, would
|
||||
# CREATE TABLE or ALTER TABLE get rolled back by a transaction? PostgreSQL,
|
||||
# SQL Server, and others support this. MySQL and others do not.
|
||||
def supports_ddl_transactions?
|
||||
false
|
||||
end
|
||||
|
||||
# Does this adapter support savepoints? PostgreSQL and MySQL do, SQLite
|
||||
# does not.
|
||||
def supports_savepoints?
|
||||
false
|
||||
end
|
||||
|
||||
# Should primary key values be selected from their corresponding
|
||||
# sequence before the insert statement? If true, next_sequence_value
|
||||
# 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 ====================================
|
||||
|
||||
# Checks whether the connection to the database is still active. This includes
|
||||
# checking whether the database is actually capable of responding, i.e. whether
|
||||
# the connection isn't stale.
|
||||
def active?
|
||||
@active != false
|
||||
end
|
||||
|
||||
# Disconnects from the database if already connected, and establishes a
|
||||
# new connection with the database.
|
||||
def reconnect!
|
||||
@active = true
|
||||
end
|
||||
|
||||
# Disconnects from the database if already connected. Otherwise, this
|
||||
# method does nothing.
|
||||
def disconnect!
|
||||
@active = false
|
||||
end
|
||||
|
||||
# Reset the state of this connection, directing the DBMS to clear
|
||||
# transactions and other connection-related server-side state. Usually a
|
||||
# database-dependent operation.
|
||||
#
|
||||
# The default implementation does nothing; the implementation should be
|
||||
# overridden by concrete adapters.
|
||||
def reset!
|
||||
# this should be overridden by concrete adapters
|
||||
end
|
||||
|
||||
# Returns true if its safe to reload the connection between requests for development mode.
|
||||
def requires_reloading?
|
||||
true
|
||||
end
|
||||
|
||||
# Checks whether the connection to the database is still active (i.e. not stale).
|
||||
# This is done under the hood by calling <tt>active?</tt>. If the connection
|
||||
# is no longer active, then this method will reconnect to the database.
|
||||
def verify!(*ignored)
|
||||
reconnect! unless active?
|
||||
end
|
||||
|
||||
# Provides access to the underlying database driver for this adapter. For
|
||||
# example, this method returns a Mysql object in case of MysqlAdapter,
|
||||
# and a PGconn object in case of PostgreSQLAdapter.
|
||||
#
|
||||
# This is useful for when you need to call a proprietary method such as
|
||||
# PostgreSQL's lo_* methods.
|
||||
def raw_connection
|
||||
@connection
|
||||
end
|
||||
|
||||
def open_transactions
|
||||
@open_transactions ||= 0
|
||||
end
|
||||
|
||||
def increment_open_transactions
|
||||
@open_transactions ||= 0
|
||||
@open_transactions += 1
|
||||
end
|
||||
|
||||
def decrement_open_transactions
|
||||
@open_transactions -= 1
|
||||
end
|
||||
|
||||
def transaction_joinable=(joinable)
|
||||
@transaction_joinable = joinable
|
||||
end
|
||||
|
||||
def create_savepoint
|
||||
end
|
||||
|
||||
def rollback_to_savepoint
|
||||
end
|
||||
|
||||
def release_savepoint
|
||||
end
|
||||
|
||||
def current_savepoint_name
|
||||
"active_record_#{open_transactions}"
|
||||
end
|
||||
|
||||
def log_info(sql, name, ms)
|
||||
if @logger && @logger.debug?
|
||||
name = '%s (%.1fms)' % [name || 'SQL', ms]
|
||||
@logger.debug(format_log_entry(name, sql.squeeze(' ')))
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
def log(sql, name)
|
||||
if block_given?
|
||||
result = nil
|
||||
ms = Benchmark.ms { result = yield }
|
||||
@runtime += ms
|
||||
log_info(sql, name, ms)
|
||||
result
|
||||
else
|
||||
log_info(sql, name, 0)
|
||||
nil
|
||||
end
|
||||
rescue SystemExit, SignalException, NoMemoryError => e
|
||||
# Don't re-wrap these exceptions. They are probably not being caused by invalid
|
||||
# sql, but rather some external stimulus beyond the responsibilty of this code.
|
||||
# Additionaly, wrapping these exceptions with StatementInvalid would lead to
|
||||
# meaningful loss of data, such as losing SystemExit#status.
|
||||
raise e
|
||||
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,662 +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') ||
|
||||
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 # def all_hashes
|
||||
rows = [] # rows = []
|
||||
each_hash { |row| rows << row } # each_hash { |row| rows << row }
|
||||
rows # rows
|
||||
end # end
|
||||
end_eval
|
||||
|
||||
# adapters before 2.7 don't have a version constant
|
||||
# and don't return null values in each_hash
|
||||
else
|
||||
target.class_eval <<-'end_eval'
|
||||
def all_hashes # def all_hashes
|
||||
rows = [] # rows = []
|
||||
all_fields = fetch_fields.inject({}) { |fields, f| # all_fields = fetch_fields.inject({}) { |fields, f|
|
||||
fields[f.name] = nil; fields # fields[f.name] = nil; fields
|
||||
} # }
|
||||
each_hash { |row| rows << all_fields.dup.update(row) } # each_hash { |row| rows << all_fields.dup.update(row) }
|
||||
rows # rows
|
||||
end # end
|
||||
end_eval
|
||||
end
|
||||
|
||||
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
|
||||
# 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
|
||||
database = config[:database]
|
||||
|
||||
# Require the MySQL driver and define Mysql::Result.all_hashes
|
||||
unless defined? Mysql
|
||||
begin
|
||||
require_library_or_gem('mysql')
|
||||
rescue LoadError
|
||||
$stderr.puts '!!! The bundled mysql.rb driver has been removed from Rails 2.2. Please install the mysql gem and try again: gem install mysql.'
|
||||
raise
|
||||
end
|
||||
end
|
||||
|
||||
MysqlCompat.define_all_hashes_method!
|
||||
|
||||
mysql = Mysql.init
|
||||
mysql.ssl_set(config[:sslkey], config[:sslcert], config[:sslca], config[:sslcapath], config[:sslcipher]) if config[:sslca] || config[:sslkey]
|
||||
|
||||
default_flags = Mysql.const_defined?(:CLIENT_MULTI_RESULTS) ? Mysql::CLIENT_MULTI_RESULTS : 0
|
||||
options = [host, username, password, database, port, socket, default_flags]
|
||||
ConnectionAdapters::MysqlAdapter.new(mysql, logger, options, config)
|
||||
end
|
||||
end
|
||||
|
||||
module ConnectionAdapters
|
||||
class MysqlColumn < Column #:nodoc:
|
||||
def extract_default(default)
|
||||
if sql_type =~ /blob/i || type == :text
|
||||
if default.blank?
|
||||
return null ? 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
|
||||
|
||||
def has_default?
|
||||
return false if sql_type =~ /blob/i || type == :text #mysql forbids defaults on blob and text columns
|
||||
super
|
||||
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)
|
||||
case sql_type
|
||||
when /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
|
||||
when /^bigint/i; 8
|
||||
when /^int/i; 4
|
||||
when /^mediumint/i; 3
|
||||
when /^smallint/i; 2
|
||||
when /^tinyint/i; 1
|
||||
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>:reconnect</tt> - Defaults to false (See MySQL documentation: http://dev.mysql.com/doc/refman/5.0/en/auto-reconnect.html).
|
||||
# * <tt>:sslca</tt> - Necessary to use MySQL with an SSL connection.
|
||||
# * <tt>:sslkey</tt> - Necessary to use MySQL with an SSL connection.
|
||||
# * <tt>:sslcert</tt> - Necessary to use MySQL with an SSL connection.
|
||||
# * <tt>:sslcapath</tt> - Necessary to use MySQL with an SSL connection.
|
||||
# * <tt>:sslcipher</tt> - Necessary to use MySQL with an SSL connection.
|
||||
#
|
||||
class MysqlAdapter < AbstractAdapter
|
||||
|
||||
##
|
||||
# :singleton-method:
|
||||
# By default, the MysqlAdapter will consider all columns of type <tt>tinyint(1)</tt>
|
||||
# as boolean. If you wish to disable this emulation (which was the default
|
||||
# behavior in versions 0.13.1 and earlier) you can add the following line
|
||||
# to your environment.rb file:
|
||||
#
|
||||
# ActiveRecord::ConnectionAdapters::MysqlAdapter.emulate_booleans = false
|
||||
cattr_accessor :emulate_booleans
|
||||
self.emulate_booleans = true
|
||||
|
||||
ADAPTER_NAME = 'MySQL'.freeze
|
||||
|
||||
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'.freeze, '0'.freeze
|
||||
|
||||
NATIVE_DATABASE_TYPES = {
|
||||
:primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY".freeze,
|
||||
:string => { :name => "varchar", :limit => 255 },
|
||||
:text => { :name => "text" },
|
||||
:integer => { :name => "int", :limit => 4 },
|
||||
: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 }
|
||||
}
|
||||
|
||||
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:
|
||||
ADAPTER_NAME
|
||||
end
|
||||
|
||||
def supports_migrations? #:nodoc:
|
||||
true
|
||||
end
|
||||
|
||||
def supports_primary_key? #:nodoc:
|
||||
true
|
||||
end
|
||||
|
||||
def supports_savepoints? #:nodoc:
|
||||
true
|
||||
end
|
||||
|
||||
def native_database_types #:nodoc:
|
||||
NATIVE_DATABASE_TYPES
|
||||
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
|
||||
|
||||
def reset!
|
||||
if @connection.respond_to?(:change_user)
|
||||
# See http://bugs.mysql.com/bug.php?id=33540 -- the workaround way to
|
||||
# reset the connection is to change the user to the same user.
|
||||
@connection.change_user(@config[:username], @config[:password], @config[:database])
|
||||
configure_connection
|
||||
end
|
||||
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
|
||||
@connection.more_results && @connection.next_result # invoking stored procedures with CLIENT_MULTI_RESULTS requires this to tidy up else connection will be dropped
|
||||
rows
|
||||
end
|
||||
|
||||
# Executes a SQL query and returns a MySQL::Result object. Note that you have to free the Result object after you're done using it.
|
||||
def execute(sql, name = nil) #:nodoc:
|
||||
log(sql, name) { @connection.query(sql) }
|
||||
rescue ActiveRecord::StatementInvalid => exception
|
||||
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 create_savepoint
|
||||
execute("SAVEPOINT #{current_savepoint_name}")
|
||||
end
|
||||
|
||||
def rollback_to_savepoint
|
||||
execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}")
|
||||
end
|
||||
|
||||
def release_savepoint
|
||||
execute("RELEASE SAVEPOINT #{current_savepoint_name}")
|
||||
end
|
||||
|
||||
def add_limit_offset!(sql, options) #:nodoc:
|
||||
if limit = options[:limit]
|
||||
limit = sanitize_limit(limit)
|
||||
unless offset = options[:offset]
|
||||
sql << " LIMIT #{limit}"
|
||||
else
|
||||
sql << " LIMIT #{offset.to_i}, #{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, options = {}) #:nodoc:
|
||||
drop_database(name)
|
||||
create_database(name, options)
|
||||
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 = []
|
||||
result = execute("SHOW TABLES", name)
|
||||
result.each { |field| tables << field[0] }
|
||||
result.free
|
||||
tables
|
||||
end
|
||||
|
||||
def drop_table(table_name, options = {})
|
||||
super(table_name, options)
|
||||
end
|
||||
|
||||
def indexes(table_name, name = nil)#:nodoc:
|
||||
indexes = []
|
||||
current_index = nil
|
||||
result = execute("SHOW KEYS FROM #{quote_table_name(table_name)}", name)
|
||||
result.each do |row|
|
||||
if current_index != row[2]
|
||||
next if row[2] == "PRIMARY" # skip the primary key
|
||||
current_index = row[2]
|
||||
indexes << IndexDefinition.new(row[0], row[2], row[1] == "0", [], [])
|
||||
end
|
||||
|
||||
indexes.last.columns << row[4]
|
||||
indexes.last.lengths << row[7]
|
||||
end
|
||||
result.free
|
||||
indexes
|
||||
end
|
||||
|
||||
def columns(table_name, name = nil)#:nodoc:
|
||||
sql = "SHOW FIELDS FROM #{quote_table_name(table_name)}"
|
||||
columns = []
|
||||
result = execute(sql, name)
|
||||
result.each { |field| columns << MysqlColumn.new(field[0], field[4], field[1], field[2] == "YES") }
|
||||
result.free
|
||||
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 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)
|
||||
add_column_position!(add_column_sql, options)
|
||||
execute(add_column_sql)
|
||||
end
|
||||
|
||||
def change_column_default(table_name, column_name, default) #:nodoc:
|
||||
column = column_for(table_name, column_name)
|
||||
change_column table_name, column_name, column.sql_type, :default => default
|
||||
end
|
||||
|
||||
def change_column_null(table_name, column_name, null, default = nil)
|
||||
column = column_for(table_name, column_name)
|
||||
|
||||
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
|
||||
|
||||
change_column table_name, column_name, column.sql_type, :null => null
|
||||
end
|
||||
|
||||
def change_column(table_name, column_name, type, options = {}) #:nodoc:
|
||||
column = column_for(table_name, column_name)
|
||||
|
||||
unless options_include_default?(options)
|
||||
options[:default] = column.default
|
||||
end
|
||||
|
||||
unless options.has_key?(:null)
|
||||
options[:null] = column.null
|
||||
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)
|
||||
add_column_position!(change_column_sql, options)
|
||||
execute(change_column_sql)
|
||||
end
|
||||
|
||||
def rename_column(table_name, column_name, new_column_name) #:nodoc:
|
||||
options = {}
|
||||
if column = columns(table_name).find { |c| c.name == column_name.to_s }
|
||||
options[:default] = column.default
|
||||
options[:null] = column.null
|
||||
else
|
||||
raise ActiveRecordError, "No such column: #{table_name}.#{column_name}"
|
||||
end
|
||||
current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'")["Type"]
|
||||
rename_column_sql = "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(new_column_name)} #{current_type}"
|
||||
add_column_options!(rename_column_sql, options)
|
||||
execute(rename_column_sql)
|
||||
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 1; 'tinyint'
|
||||
when 2; 'smallint'
|
||||
when 3; 'mediumint'
|
||||
when nil, 4, 11; 'int(11)' # compatibility with MySQL default
|
||||
when 5..8; 'bigint'
|
||||
else raise(ActiveRecordError, "No integer type has byte size #{limit}")
|
||||
end
|
||||
end
|
||||
|
||||
def add_column_position!(sql, options)
|
||||
if options[:first]
|
||||
sql << " FIRST"
|
||||
elsif options[:after]
|
||||
sql << " AFTER #{quote_column_name(options[:after])}"
|
||||
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 = []
|
||||
result = execute("describe #{quote_table_name(table)}")
|
||||
result.each_hash do |h|
|
||||
keys << h["Field"]if h["Key"] == "PRI"
|
||||
end
|
||||
result.free
|
||||
keys.length == 1 ? [keys.first, nil] : nil
|
||||
end
|
||||
|
||||
# Returns just a table's primary key
|
||||
def primary_key(table)
|
||||
pk_and_sequence = pk_and_sequence_for(table)
|
||||
pk_and_sequence && pk_and_sequence.first
|
||||
end
|
||||
|
||||
def case_sensitive_equality_operator
|
||||
"= BINARY"
|
||||
end
|
||||
|
||||
def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key)
|
||||
where_sql
|
||||
end
|
||||
|
||||
protected
|
||||
def quoted_columns_for_index(column_names, options = {})
|
||||
length = options[:length] if options.is_a?(Hash)
|
||||
|
||||
quoted_column_names = case length
|
||||
when Hash
|
||||
column_names.map {|name| length[name] ? "#{quote_column_name(name)}(#{length[name]})" : quote_column_name(name) }
|
||||
when Fixnum
|
||||
column_names.map {|name| "#{quote_column_name(name)}(#{length})"}
|
||||
else
|
||||
column_names.map {|name| quote_column_name(name) }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def connect
|
||||
encoding = @config[:encoding]
|
||||
if encoding
|
||||
@connection.options(Mysql::SET_CHARSET_NAME, encoding) rescue nil
|
||||
end
|
||||
|
||||
if @config[:sslca] || @config[:sslkey]
|
||||
@connection.ssl_set(@config[:sslkey], @config[:sslcert], @config[:sslca], @config[:sslcapath], @config[:sslcipher])
|
||||
end
|
||||
|
||||
@connection.options(Mysql::OPT_CONNECT_TIMEOUT, @config[:connect_timeout]) if @config[:connect_timeout]
|
||||
@connection.options(Mysql::OPT_READ_TIMEOUT, @config[:read_timeout]) if @config[:read_timeout]
|
||||
@connection.options(Mysql::OPT_WRITE_TIMEOUT, @config[:write_timeout]) if @config[:write_timeout]
|
||||
|
||||
@connection.real_connect(*@connection_options)
|
||||
|
||||
# reconnect must be set after real_connect is called, because real_connect sets it to false internally
|
||||
@connection.reconnect = !!@config[:reconnect] if @connection.respond_to?(:reconnect=)
|
||||
|
||||
configure_connection
|
||||
end
|
||||
|
||||
def configure_connection
|
||||
encoding = @config[:encoding]
|
||||
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
|
||||
@connection.more_results && @connection.next_result # invoking stored procedures with CLIENT_MULTI_RESULTS requires this to tidy up else connection will be dropped
|
||||
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
|
||||
|
||||
def column_for(table_name, column_name)
|
||||
unless column = columns(table_name).find { |c| c.name == column_name.to_s }
|
||||
raise "No such column: #{table_name}.#{column_name}"
|
||||
end
|
||||
column
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -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, config)
|
||||
end
|
||||
end
|
||||
|
||||
module ConnectionAdapters #:nodoc:
|
||||
class SQLite3Adapter < SQLiteAdapter # :nodoc:
|
||||
def table_structure(table_name)
|
||||
@connection.table_info(quote_table_name(table_name)).tap do |structure|
|
||||
raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if structure.empty?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,454 +0,0 @@
|
|||
# encoding: binary
|
||||
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
|
||||
|
||||
message = "Support for SQLite2Adapter and DeprecatedSQLiteAdapter has been removed from Rails 3. "
|
||||
message << "You should migrate to SQLite 3+ or use the plugin from git://github.com/rails/sqlite2_adapter.git with Rails 3."
|
||||
ActiveSupport::Deprecation.warn(message)
|
||||
|
||||
# "Downgrade" deprecated sqlite API
|
||||
if SQLite.const_defined?(:Version)
|
||||
ConnectionAdapters::SQLite2Adapter.new(db, logger, config)
|
||||
else
|
||||
ConnectionAdapters::DeprecatedSQLiteAdapter.new(db, logger, config)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def parse_sqlite_config!(config)
|
||||
if config.include?(:dbfile)
|
||||
ActiveSupport::Deprecation.warn "Please update config/database.yml to use 'database' instead of 'dbfile'"
|
||||
end
|
||||
|
||||
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 = value.dup.force_encoding(Encoding::BINARY) if value.respond_to?(:force_encoding)
|
||||
value.gsub(/\0|\%/n) do |b|
|
||||
case b
|
||||
when "\0" then "%00"
|
||||
when "%" then "%25"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def binary_to_string(value)
|
||||
value = value.dup.force_encoding(Encoding::BINARY) if value.respond_to?(:force_encoding)
|
||||
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
|
||||
class Version
|
||||
include Comparable
|
||||
|
||||
def initialize(version_string)
|
||||
@version = version_string.split('.').map(&:to_i)
|
||||
end
|
||||
|
||||
def <=>(version_string)
|
||||
@version <=> version_string.split('.').map(&:to_i)
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(connection, logger, config)
|
||||
super(connection, logger)
|
||||
@config = config
|
||||
end
|
||||
|
||||
def adapter_name #:nodoc:
|
||||
'SQLite'
|
||||
end
|
||||
|
||||
def supports_ddl_transactions?
|
||||
sqlite_version >= '2.0.0'
|
||||
end
|
||||
|
||||
def supports_migrations? #:nodoc:
|
||||
true
|
||||
end
|
||||
|
||||
def supports_primary_key? #:nodoc:
|
||||
true
|
||||
end
|
||||
|
||||
def requires_reloading?
|
||||
true
|
||||
end
|
||||
|
||||
def supports_add_column?
|
||||
sqlite_version >= '3.1.6'
|
||||
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['name']
|
||||
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'].to_i == 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'].to_i != 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, index_name) #:nodoc:
|
||||
execute "DROP INDEX #{quote_column_name(index_name)}"
|
||||
end
|
||||
|
||||
def rename_table(name, new_name)
|
||||
execute "ALTER TABLE #{name} RENAME TO #{new_name}"
|
||||
end
|
||||
|
||||
# See: http://www.sqlite.org/lang_altertable.html
|
||||
# SQLite has an additional restriction on the ALTER TABLE statement
|
||||
def valid_alter_table_options( type, options)
|
||||
type.to_sym != :primary_key
|
||||
end
|
||||
|
||||
def add_column(table_name, column_name, type, options = {}) #:nodoc:
|
||||
if supports_add_column? && valid_alter_table_options( type, options )
|
||||
super(table_name, column_name, type, options)
|
||||
else
|
||||
alter_table(table_name) do |definition|
|
||||
definition.column(column_name, type, options)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def remove_column(table_name, *column_names) #:nodoc:
|
||||
raise ArgumentError.new("You must specify at least one column name. Example: remove_column(:people, :first_name)") if column_names.empty?
|
||||
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_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
|
||||
alter_table(table_name) do |definition|
|
||||
definition[column_name].null = null
|
||||
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:
|
||||
unless columns(table_name).detect{|c| c.name == column_name.to_s }
|
||||
raise ActiveRecord::ActiveRecordError, "Missing column #{table_name}.#{column_name}"
|
||||
end
|
||||
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)
|
||||
execute("PRAGMA table_info(#{quote_table_name(table_name)})").tap do |structure|
|
||||
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? && 'id' == primary_key(from).to_s))
|
||||
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 ||= SQLiteAdapter::Version.new(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 rename_table(name, new_name)
|
||||
move_table(name, new_name)
|
||||
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
|
||||
183
vendor/rails/activerecord/lib/active_record/dirty.rb
vendored
183
vendor/rails/activerecord/lib/active_record/dirty.rb
vendored
|
|
@ -1,183 +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
|
||||
DIRTY_SUFFIXES = ['_changed?', '_change', '_will_change!', '_was']
|
||||
|
||||
def self.included(base)
|
||||
base.attribute_method_suffix *DIRTY_SUFFIXES
|
||||
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.class_attribute :partial_updates
|
||||
base.partial_updates = true
|
||||
|
||||
base.send(:extend, ClassMethods)
|
||||
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 <tt>attr => original value</tt>.
|
||||
def changed_attributes
|
||||
@changed_attributes ||= {}
|
||||
end
|
||||
|
||||
# Handle <tt>*_changed?</tt> for +method_missing+.
|
||||
def attribute_changed?(attr)
|
||||
changed_attributes.include?(attr)
|
||||
end
|
||||
|
||||
# Handle <tt>*_change</tt> for +method_missing+.
|
||||
def attribute_change(attr)
|
||||
[changed_attributes[attr], __send__(attr)] if attribute_changed?(attr)
|
||||
end
|
||||
|
||||
# Handle <tt>*_was</tt> for +method_missing+.
|
||||
def attribute_was(attr)
|
||||
attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr)
|
||||
end
|
||||
|
||||
# Handle <tt>*_will_change!</tt> 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.
|
||||
if changed_attributes.include?(attr)
|
||||
old = changed_attributes[attr]
|
||||
changed_attributes.delete(attr) unless field_changed?(attr, old, value)
|
||||
else
|
||||
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?
|
||||
# Serialized attributes should always be written in case they've been
|
||||
# changed in place.
|
||||
update_without_dirty(changed | (attributes.keys & self.class.serialized_attributes.keys))
|
||||
else
|
||||
update_without_dirty
|
||||
end
|
||||
end
|
||||
|
||||
def field_changed?(attr, old, value)
|
||||
if column = column_for_attribute(attr)
|
||||
if column.number? && column.null && (old.nil? || old == 0) && value.blank?
|
||||
# For nullable numeric columns, NULL gets stored in database for blank (i.e. '') values.
|
||||
# Hence we don't record it as a change if the value changes from nil to ''.
|
||||
# If an old value of 0 is set to '' we want this to get changed to nil as otherwise it'll
|
||||
# be typecast back to 0 (''.to_i => 0)
|
||||
value = nil
|
||||
else
|
||||
value = column.type_cast(value)
|
||||
end
|
||||
end
|
||||
|
||||
old != value
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
def self.extended(base)
|
||||
base.singleton_class.alias_method_chain(:alias_attribute, :dirty)
|
||||
end
|
||||
|
||||
def alias_attribute_with_dirty(new_name, old_name)
|
||||
alias_attribute_without_dirty(new_name, old_name)
|
||||
DIRTY_SUFFIXES.each do |suffix|
|
||||
module_eval <<-STR, __FILE__, __LINE__ + 1
|
||||
def #{new_name}#{suffix}; self.#{old_name}#{suffix}; end # def subject_changed?; self.title_changed?; end
|
||||
STR
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
module ActiveRecord
|
||||
class DynamicFinderMatch
|
||||
def self.match(method)
|
||||
df_match = self.new(method)
|
||||
df_match.finder ? df_match : nil
|
||||
end
|
||||
|
||||
def initialize(method)
|
||||
@finder = :first
|
||||
case method.to_s
|
||||
when /^find_(all_by|last_by|by)_([_a-zA-Z]\w*)$/
|
||||
@finder = :last if $1 == 'last_by'
|
||||
@finder = :all if $1 == 'all_by'
|
||||
names = $2
|
||||
when /^find_by_([_a-zA-Z]\w*)\!$/
|
||||
@bang = true
|
||||
names = $1
|
||||
when /^find_or_(initialize|create)_by_([_a-zA-Z]\w*)$/
|
||||
@instantiator = $1 == 'initialize' ? :new : :create
|
||||
names = $2
|
||||
else
|
||||
@finder = nil
|
||||
end
|
||||
@attribute_names = names && names.split('_and_')
|
||||
end
|
||||
|
||||
attr_reader :finder, :attribute_names, :instantiator
|
||||
|
||||
def finder?
|
||||
!@finder.nil? && @instantiator.nil?
|
||||
end
|
||||
|
||||
def instantiator?
|
||||
@finder == :first && !@instantiator.nil?
|
||||
end
|
||||
|
||||
def bang?
|
||||
@bang
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
module ActiveRecord
|
||||
class DynamicScopeMatch
|
||||
def self.match(method)
|
||||
ds_match = self.new(method)
|
||||
ds_match.scope ? ds_match : nil
|
||||
end
|
||||
|
||||
def initialize(method)
|
||||
@scope = true
|
||||
case method.to_s
|
||||
when /^scoped_by_([_a-zA-Z]\w*)$/
|
||||
names = $1
|
||||
else
|
||||
@scope = nil
|
||||
end
|
||||
@attribute_names = names && names.split('_and_')
|
||||
end
|
||||
|
||||
attr_reader :scope, :attribute_names
|
||||
|
||||
def scope?
|
||||
!@scope.nil?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,997 +0,0 @@
|
|||
require 'erb'
|
||||
require 'yaml'
|
||||
require 'csv'
|
||||
require 'zlib'
|
||||
require 'active_support/dependencies'
|
||||
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.
|
||||
#
|
||||
# = Fixture formats
|
||||
#
|
||||
# Fixtures 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 (CSV) 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 in testcases
|
||||
#
|
||||
# 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 'test_helper'
|
||||
#
|
||||
# class WebSiteTest < ActiveSupport::TestCase
|
||||
# test "web_site_count" do
|
||||
# assert_equal 2, WebSite.count
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# By default, the <tt>test_helper module</tt> will load all of your fixtures into your test database, so this test will succeed.
|
||||
# The testing environment will automatically load the all 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 fixture's data may also be accessed by
|
||||
# using a special dynamic method, which has the same name as the model, and accepts the
|
||||
# name of the fixture to instantiate:
|
||||
#
|
||||
# test "find" do
|
||||
# assert_equal "Ruby on Rails", web_sites(:rubyonrails).name
|
||||
# end
|
||||
#
|
||||
# Alternatively, you may enable auto-instantiation of the fixture data. For instance, take the following tests:
|
||||
#
|
||||
# test "find_alt_method_1" do
|
||||
# assert_equal "Ruby on Rails", @web_sites['rubyonrails']['name']
|
||||
# end
|
||||
#
|
||||
# test "find_alt_method_2" do
|
||||
# assert_equal "Ruby on Rails", @rubyonrails.news
|
||||
# end
|
||||
#
|
||||
# In order to use these methods to access fixtured data within your testcases, you must specify one of the
|
||||
# following in your <tt>ActiveSupport::TestCase</tt>-derived class:
|
||||
#
|
||||
# - to fully enable instantiated fixtures (enable alternate methods #1 and #2 above)
|
||||
# self.use_instantiated_fixtures = true
|
||||
#
|
||||
# - create only the hash for the fixtures, do not 'find' each instance (enable alternate method #1 only)
|
||||
# self.use_instantiated_fixtures = :no_instances
|
||||
#
|
||||
# Using either of these alternate methods incurs a performance hit, as the fixtured data must be fully
|
||||
# traversed in the database to create the fixture hash and/or instance variables. This is expensive for
|
||||
# large sets of fixtured data.
|
||||
#
|
||||
# = 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.
|
||||
#
|
||||
# class FooTest < ActiveSupport::TestCase
|
||||
# self.use_transactional_fixtures = true
|
||||
#
|
||||
# test "godzilla" do
|
||||
# assert !Foo.find(:all).empty?
|
||||
# Foo.destroy_all
|
||||
# assert Foo.find(:all).empty?
|
||||
# end
|
||||
#
|
||||
# test "godzilla aftermath" do
|
||||
# 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 IDs
|
||||
# * 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 IDs
|
||||
#
|
||||
# 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 IDs:
|
||||
#
|
||||
# ### 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)
|
||||
MAX_ID = 2 ** 30 - 1
|
||||
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(:requires_new => true) 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, platform-independent identifier for +label+.
|
||||
# Identifiers are positive integers less than 2^32.
|
||||
def self.identify(label)
|
||||
Zlib.crc32(label.to_s) % MAX_ID
|
||||
end
|
||||
|
||||
attr_reader :table_name, :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
|
||||
@name = table_name # preserve fixture base name
|
||||
@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, @connection)
|
||||
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, @connection)
|
||||
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, @connection)
|
||||
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, connection = ActiveRecord::Base.connection)
|
||||
@connection = connection
|
||||
@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| @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 << @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 ActiveRecord
|
||||
module TestFixtures
|
||||
def self.included(base)
|
||||
base.class_eval do
|
||||
setup :setup_fixtures
|
||||
teardown :teardown_fixtures
|
||||
|
||||
superclass_delegating_accessor :fixture_path
|
||||
superclass_delegating_accessor :fixture_table_names
|
||||
superclass_delegating_accessor :fixture_class_names
|
||||
superclass_delegating_accessor :use_transactional_fixtures
|
||||
superclass_delegating_accessor :use_instantiated_fixtures # true, false, or :no_instances
|
||||
superclass_delegating_accessor :pre_loaded_fixtures
|
||||
|
||||
self.fixture_table_names = []
|
||||
self.use_transactional_fixtures = false
|
||||
self.use_instantiated_fixtures = true
|
||||
self.pre_loaded_fixtures = false
|
||||
|
||||
self.fixture_class_names = {}
|
||||
end
|
||||
|
||||
base.extend ClassMethods
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
def set_fixture_class(class_names = {})
|
||||
self.fixture_class_names = self.fixture_class_names.merge(class_names)
|
||||
end
|
||||
|
||||
def fixtures(*table_names)
|
||||
if table_names.first == :all
|
||||
table_names = Dir["#{fixture_path}/*.yml"] + Dir["#{fixture_path}/*.csv"]
|
||||
table_names.map! { |f| File.basename(f).split('.')[0..-2].join('.') }
|
||||
else
|
||||
table_names = table_names.flatten.map { |n| n.to_s }
|
||||
end
|
||||
|
||||
self.fixture_table_names |= table_names
|
||||
require_fixture_classes(table_names)
|
||||
setup_fixture_accessors(table_names)
|
||||
end
|
||||
|
||||
def try_to_load_dependency(file_name)
|
||||
require_dependency file_name
|
||||
rescue LoadError => e
|
||||
# Let's hope the developer has included it himself
|
||||
|
||||
# Let's warn in case this is a subdependency, otherwise
|
||||
# subdependency error messages are totally cryptic
|
||||
if ActiveRecord::Base.logger
|
||||
ActiveRecord::Base.logger.warn("Unable to load #{file_name}, underlying cause #{e.message} \n\n #{e.backtrace.join("\n")}")
|
||||
end
|
||||
end
|
||||
|
||||
def require_fixture_classes(table_names = nil)
|
||||
(table_names || fixture_table_names).each do |table_name|
|
||||
file_name = table_name.to_s
|
||||
file_name = file_name.singularize if ActiveRecord::Base.pluralize_table_names
|
||||
try_to_load_dependency(file_name)
|
||||
end
|
||||
end
|
||||
|
||||
def setup_fixture_accessors(table_names = nil)
|
||||
table_names = [table_names] if table_names && !table_names.respond_to?(:each)
|
||||
(table_names || fixture_table_names).each do |table_name|
|
||||
table_name = table_name.to_s.tr('.', '_')
|
||||
|
||||
define_method(table_name) do |*fixtures|
|
||||
force_reload = fixtures.pop if fixtures.last == true || fixtures.last == :reload
|
||||
|
||||
@fixture_cache[table_name] ||= {}
|
||||
|
||||
instances = fixtures.map do |fixture|
|
||||
@fixture_cache[table_name].delete(fixture) if force_reload
|
||||
|
||||
if @loaded_fixtures[table_name][fixture.to_s]
|
||||
@fixture_cache[table_name][fixture] ||= @loaded_fixtures[table_name][fixture.to_s].find
|
||||
else
|
||||
raise StandardError, "No fixture with name '#{fixture}' found for table '#{table_name}'"
|
||||
end
|
||||
end
|
||||
|
||||
instances.size == 1 ? instances.first : instances
|
||||
end
|
||||
private table_name
|
||||
end
|
||||
end
|
||||
|
||||
def uses_transaction(*methods)
|
||||
@uses_transaction = [] unless defined?(@uses_transaction)
|
||||
@uses_transaction.concat methods.map(&:to_s)
|
||||
end
|
||||
|
||||
def uses_transaction?(method)
|
||||
@uses_transaction = [] unless defined?(@uses_transaction)
|
||||
@uses_transaction.include?(method.to_s)
|
||||
end
|
||||
end
|
||||
|
||||
def run_in_transaction?
|
||||
use_transactional_fixtures &&
|
||||
!self.class.uses_transaction?(method_name)
|
||||
end
|
||||
|
||||
def setup_fixtures
|
||||
return unless defined?(ActiveRecord) && !ActiveRecord::Base.configurations.blank?
|
||||
|
||||
if pre_loaded_fixtures && !use_transactional_fixtures
|
||||
raise RuntimeError, 'pre_loaded_fixtures requires use_transactional_fixtures'
|
||||
end
|
||||
|
||||
@fixture_cache = {}
|
||||
@@already_loaded_fixtures ||= {}
|
||||
|
||||
# Load fixtures once and begin transaction.
|
||||
if run_in_transaction?
|
||||
if @@already_loaded_fixtures[self.class]
|
||||
@loaded_fixtures = @@already_loaded_fixtures[self.class]
|
||||
else
|
||||
load_fixtures
|
||||
@@already_loaded_fixtures[self.class] = @loaded_fixtures
|
||||
end
|
||||
ActiveRecord::Base.connection.increment_open_transactions
|
||||
ActiveRecord::Base.connection.transaction_joinable = false
|
||||
ActiveRecord::Base.connection.begin_db_transaction
|
||||
# Load fixtures for every test.
|
||||
else
|
||||
Fixtures.reset_cache
|
||||
@@already_loaded_fixtures[self.class] = nil
|
||||
load_fixtures
|
||||
end
|
||||
|
||||
# Instantiate fixtures for every test if requested.
|
||||
instantiate_fixtures if use_instantiated_fixtures
|
||||
end
|
||||
|
||||
def teardown_fixtures
|
||||
return unless defined?(ActiveRecord) && !ActiveRecord::Base.configurations.blank?
|
||||
|
||||
unless run_in_transaction?
|
||||
Fixtures.reset_cache
|
||||
end
|
||||
|
||||
# Rollback changes if a transaction is active.
|
||||
if run_in_transaction? && ActiveRecord::Base.connection.open_transactions != 0
|
||||
ActiveRecord::Base.connection.rollback_db_transaction
|
||||
ActiveRecord::Base.connection.decrement_open_transactions
|
||||
end
|
||||
ActiveRecord::Base.clear_active_connections!
|
||||
end
|
||||
|
||||
private
|
||||
def load_fixtures
|
||||
@loaded_fixtures = {}
|
||||
fixtures = Fixtures.create_fixtures(fixture_path, fixture_table_names, fixture_class_names)
|
||||
unless fixtures.nil?
|
||||
if fixtures.instance_of?(Fixtures)
|
||||
@loaded_fixtures[fixtures.name] = fixtures
|
||||
else
|
||||
fixtures.each { |f| @loaded_fixtures[f.name] = f }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# for pre_loaded_fixtures, only require the classes once. huge speed improvement
|
||||
@@required_fixture_classes = false
|
||||
|
||||
def instantiate_fixtures
|
||||
if pre_loaded_fixtures
|
||||
raise RuntimeError, 'Load fixtures before instantiating them.' if Fixtures.all_loaded_fixtures.empty?
|
||||
unless @@required_fixture_classes
|
||||
self.class.require_fixture_classes Fixtures.all_loaded_fixtures.keys
|
||||
@@required_fixture_classes = true
|
||||
end
|
||||
Fixtures.instantiate_all_loaded_fixtures(self, load_instances?)
|
||||
else
|
||||
raise RuntimeError, 'Load fixtures before instantiating them.' if @loaded_fixtures.nil?
|
||||
@loaded_fixtures.each do |table_name, fixtures|
|
||||
Fixtures.instantiate_fixtures(self, table_name, fixtures, load_instances?)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def load_instances?
|
||||
use_instantiated_fixtures != :no_instances
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
en:
|
||||
activerecord:
|
||||
errors:
|
||||
# The values :model, :attribute and :value are always available for interpolation
|
||||
# The value :count is available when applicable. Can be used for pluralization.
|
||||
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 %{count} characters)"
|
||||
too_short: "is too short (minimum is %{count} characters)"
|
||||
wrong_length: "is the wrong length (should be %{count} characters)"
|
||||
taken: "has already been taken"
|
||||
not_a_number: "is not a number"
|
||||
greater_than: "must be greater than %{count}"
|
||||
greater_than_or_equal_to: "must be greater than or equal to %{count}"
|
||||
equal_to: "must be equal to %{count}"
|
||||
less_than: "must be less than %{count}"
|
||||
less_than_or_equal_to: "must be less than or equal to %{count}"
|
||||
odd: "must be odd"
|
||||
even: "must be even"
|
||||
record_invalid: "Validation failed: %{errors}"
|
||||
# Append your own errors here or at the model/attributes scope.
|
||||
|
||||
full_messages:
|
||||
format: "%{attribute} %{message}"
|
||||
|
||||
# You can define own errors for models or model attributes.
|
||||
# The values :model, :attribute and :value are always available for interpolation.
|
||||
#
|
||||
# For example,
|
||||
# models:
|
||||
# user:
|
||||
# blank: "This is a custom blank message for %{model}: %{attribute}"
|
||||
# attributes:
|
||||
# login:
|
||||
# blank: "This is a custom blank message for User login"
|
||||
# Will define custom blank validation message for User model and
|
||||
# custom blank validation message for login attribute of User model.
|
||||
#models:
|
||||
|
||||
# Translate model names. Used in Model.human_name().
|
||||
#models:
|
||||
# For example,
|
||||
# user: "Dude"
|
||||
# will translate User model name to "Dude"
|
||||
|
||||
# Translate model attribute names. Used in Model.human_attribute_name(attribute).
|
||||
#attributes:
|
||||
# For example,
|
||||
# user:
|
||||
# login: "Handle"
|
||||
# will translate User attribute "login" as "Handle"
|
||||
|
||||
|
|
@ -1,182 +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
|
||||
#
|
||||
# Optimistic locking will also check for stale data when objects are destroyed. Example:
|
||||
#
|
||||
# p1 = Person.find(1)
|
||||
# p2 = Person.find(1)
|
||||
#
|
||||
# p1.first_name = "Michael"
|
||||
# p1.save
|
||||
#
|
||||
# p2.destroy # 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 :destroy, :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?
|
||||
return 0 if attribute_names.empty?
|
||||
|
||||
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: #{self.class.name}"
|
||||
end
|
||||
|
||||
affected_rows
|
||||
|
||||
# If something went wrong, revert the version.
|
||||
rescue Exception
|
||||
send(lock_col + '=', previous_value)
|
||||
raise
|
||||
end
|
||||
end
|
||||
|
||||
def destroy_with_lock #:nodoc:
|
||||
return destroy_without_lock unless locking_enabled?
|
||||
|
||||
unless new_record?
|
||||
lock_col = self.class.locking_column
|
||||
previous_value = send(lock_col).to_i
|
||||
|
||||
affected_rows = connection.delete(
|
||||
"DELETE FROM #{self.class.quoted_table_name} " +
|
||||
"WHERE #{connection.quote_column_name(self.class.primary_key)} = #{quoted_id} " +
|
||||
"AND #{self.class.quoted_locking_column} = #{quote_value(previous_value)}",
|
||||
"#{self.class.name} Destroy"
|
||||
)
|
||||
|
||||
unless affected_rows == 1
|
||||
raise ActiveRecord::StaleObjectError, "Attempted to delete a stale object: #{self.class.name}"
|
||||
end
|
||||
end
|
||||
|
||||
@destroyed = true
|
||||
freeze
|
||||
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,55 +0,0 @@
|
|||
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,571 +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. It will also
|
||||
# invoke the db:schema:dump task, which will update your db/schema.rb file
|
||||
# to match the structure of your database.
|
||||
#
|
||||
# To roll the database back to a previous migration version, use
|
||||
# <tt>rake db:migrate VERSION=X</tt> where <tt>X</tt> is the version to which
|
||||
# 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.
|
||||
#
|
||||
# == Timestamped Migrations
|
||||
#
|
||||
# By default, Rails generates migrations that look like:
|
||||
#
|
||||
# 20080717013526_your_migration_name.rb
|
||||
#
|
||||
# The prefix is a generation timestamp (in UTC).
|
||||
#
|
||||
# If you'd prefer to use numeric prefixes, you can turn timestamped migrations
|
||||
# off by setting:
|
||||
#
|
||||
# config.active_record.timestamped_migrations = false
|
||||
#
|
||||
# In environment.rb.
|
||||
#
|
||||
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 connection
|
||||
ActiveRecord::Base.connection
|
||||
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
|
||||
connection.send(method, *arguments, &block)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# MigrationProxy is used to defer loading of the actual migration classes
|
||||
# until they are needed
|
||||
class MigrationProxy
|
||||
|
||||
attr_accessor :name, :version, :filename
|
||||
|
||||
delegate :migrate, :announce, :write, :to=>:migration
|
||||
|
||||
private
|
||||
|
||||
def migration
|
||||
@migration ||= load_migration
|
||||
end
|
||||
|
||||
def load_migration
|
||||
load(filename)
|
||||
name.constantize
|
||||
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 == 0 && target_version == 0 then # noop
|
||||
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 migrations_path
|
||||
'db/migrate'
|
||||
end
|
||||
|
||||
def schema_migrations_table_name
|
||||
Base.table_name_prefix + 'schema_migrations' + Base.table_name_suffix
|
||||
end
|
||||
|
||||
def get_all_versions
|
||||
Base.connection.select_values("SELECT version FROM #{schema_migrations_table_name}").map(&:to_i).sort
|
||||
end
|
||||
|
||||
def current_version
|
||||
sm_table = schema_migrations_table_name
|
||||
if Base.connection.table_exists?(sm_table)
|
||||
get_all_versions.max || 0
|
||||
else
|
||||
0
|
||||
end
|
||||
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
|
||||
migrated.last || 0
|
||||
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?
|
||||
unless (up? && migrated.include?(target.version.to_i)) || (down? && !migrated.include?(target.version.to_i))
|
||||
target.migrate(@direction)
|
||||
record_version_state_after_migrating(target.version)
|
||||
end
|
||||
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.name} (#{migration.version})"
|
||||
|
||||
# On our way up, we skip migrating the ones we've already migrated
|
||||
next if up? && migrated.include?(migration.version.to_i)
|
||||
|
||||
# On our way down, we skip reverting the ones we've never migrated
|
||||
if down? && !migrated.include?(migration.version.to_i)
|
||||
migration.announce 'never migrated, skipping'; migration.write
|
||||
next
|
||||
end
|
||||
|
||||
begin
|
||||
ddl_transaction do
|
||||
migration.migrate(@direction)
|
||||
record_version_state_after_migrating(migration.version)
|
||||
end
|
||||
rescue => e
|
||||
canceled_msg = Base.connection.supports_ddl_transactions? ? "this and " : ""
|
||||
raise StandardError, "An error has occurred, #{canceled_msg}all later migrations canceled:\n\n#{e}", e.backtrace
|
||||
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
|
||||
|
||||
klasses << (MigrationProxy.new).tap do |migration|
|
||||
migration.name = name.camelize
|
||||
migration.version = version
|
||||
migration.filename = file
|
||||
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
|
||||
@migrated_versions ||= self.class.get_all_versions
|
||||
end
|
||||
|
||||
private
|
||||
def record_version_state_after_migrating(version)
|
||||
sm_table = self.class.schema_migrations_table_name
|
||||
|
||||
@migrated_versions ||= []
|
||||
if down?
|
||||
@migrated_versions.delete(version.to_i)
|
||||
Base.connection.update("DELETE FROM #{sm_table} WHERE version = '#{version}'")
|
||||
else
|
||||
@migrated_versions.push(version.to_i).sort!
|
||||
Base.connection.insert("INSERT INTO #{sm_table} (version) VALUES ('#{version}')")
|
||||
end
|
||||
end
|
||||
|
||||
def up?
|
||||
@direction == :up
|
||||
end
|
||||
|
||||
def down?
|
||||
@direction == :down
|
||||
end
|
||||
|
||||
# Wrap the migration in a transaction only if supported by the adapter.
|
||||
def ddl_transaction(&block)
|
||||
if Base.connection.supports_ddl_transactions?
|
||||
Base.transaction { block.call }
|
||||
else
|
||||
block.call
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,197 +0,0 @@
|
|||
module ActiveRecord
|
||||
module NamedScope
|
||||
# All subclasses of ActiveRecord::Base have one named scope:
|
||||
# * <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.
|
||||
#
|
||||
# You can define a scope that applies to all finders using ActiveRecord::Base.default_scope.
|
||||
def self.included(base)
|
||||
base.extend ClassMethods
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
def scopes
|
||||
read_inheritable_attribute(:scopes) || write_inheritable_attribute(:scopes, {})
|
||||
end
|
||||
|
||||
def scoped(scope, &block)
|
||||
Scope.new(self, scope, &block)
|
||||
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 Shirt.red and Shirt.dry_clean_only. Shirt.red,
|
||||
# in effect, represents the query <tt>Shirt.find(:all, :conditions => {:color => 'red'})</tt>.
|
||||
#
|
||||
# Unlike <tt>Shirt.find(...)</tt>, however, the object returned by Shirt.red 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, named \scopes act 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 was 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 descendant 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)
|
||||
name = name.to_sym
|
||||
|
||||
scopes[name] = lambda do |parent_scope, *args|
|
||||
Scope.new(parent_scope, case options
|
||||
when Hash
|
||||
options
|
||||
when Proc
|
||||
if self.model_name != parent_scope.model_name
|
||||
options.bind(parent_scope).call(*args)
|
||||
else
|
||||
options.call(*args)
|
||||
end
|
||||
end, &block)
|
||||
end
|
||||
|
||||
singleton_class.send :define_method, name do |*args|
|
||||
scopes[name].call(self, *args)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class Scope
|
||||
attr_reader :proxy_scope, :proxy_options, :current_scoped_methods_when_defined
|
||||
NON_DELEGATE_METHODS = %w(nil? send object_id class extend find size count sum average maximum minimum paginate first last empty? any? respond_to?).to_set
|
||||
[].methods.each do |m|
|
||||
unless m =~ /^__/ || NON_DELEGATE_METHODS.include?(m.to_s)
|
||||
delegate m, :to => :proxy_found
|
||||
end
|
||||
end
|
||||
|
||||
delegate :scopes, :with_scope, :scoped_methods, :to => :proxy_scope
|
||||
|
||||
def initialize(proxy_scope, options, &block)
|
||||
options ||= {}
|
||||
[options[:extend]].flatten.each { |extension| extend extension } if options[:extend]
|
||||
extend Module.new(&block) if block_given?
|
||||
unless Scope === proxy_scope
|
||||
@current_scoped_methods_when_defined = proxy_scope.send(:current_scoped_methods)
|
||||
end
|
||||
@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 size
|
||||
@found ? @found.length : count
|
||||
end
|
||||
|
||||
def empty?
|
||||
@found ? @found.empty? : count.zero?
|
||||
end
|
||||
|
||||
def respond_to?(method, include_private = false)
|
||||
super || @proxy_scope.respond_to?(method, include_private)
|
||||
end
|
||||
|
||||
def any?
|
||||
if block_given?
|
||||
proxy_found.any? { |*block_args| yield(*block_args) }
|
||||
else
|
||||
!empty?
|
||||
end
|
||||
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, :create => proxy_options[:conditions].is_a?(Hash) ? proxy_options[:conditions] : {}}, :reverse_merge) do
|
||||
method = :new if method == :build
|
||||
if current_scoped_methods_when_defined && !scoped_methods.include?(current_scoped_methods_when_defined)
|
||||
with_scope current_scoped_methods_when_defined do
|
||||
proxy_scope.send(method, *args, &block)
|
||||
end
|
||||
else
|
||||
proxy_scope.send(method, *args, &block)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def load_found
|
||||
@found = find(:all)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,407 +0,0 @@
|
|||
module ActiveRecord
|
||||
module NestedAttributes #:nodoc:
|
||||
class TooManyRecords < ActiveRecordError
|
||||
end
|
||||
|
||||
def self.included(base)
|
||||
base.extend(ClassMethods)
|
||||
base.class_inheritable_accessor :nested_attributes_options, :instance_writer => false
|
||||
base.nested_attributes_options = {}
|
||||
end
|
||||
|
||||
# == Nested Attributes
|
||||
#
|
||||
# Nested attributes allow you to save attributes on associated records
|
||||
# through the parent. By default nested attribute updating is turned off,
|
||||
# you can enable it using the accepts_nested_attributes_for class method.
|
||||
# When you enable nested attributes an attribute writer is defined on
|
||||
# the model.
|
||||
#
|
||||
# The attribute writer is named after the association, which means that
|
||||
# in the following example, two new methods are added to your model:
|
||||
# <tt>author_attributes=(attributes)</tt> and
|
||||
# <tt>pages_attributes=(attributes)</tt>.
|
||||
#
|
||||
# class Book < ActiveRecord::Base
|
||||
# has_one :author
|
||||
# has_many :pages
|
||||
#
|
||||
# accepts_nested_attributes_for :author, :pages
|
||||
# end
|
||||
#
|
||||
# Note that the <tt>:autosave</tt> option is automatically enabled on every
|
||||
# association that accepts_nested_attributes_for is used for.
|
||||
#
|
||||
# === One-to-one
|
||||
#
|
||||
# Consider a Member model that has one Avatar:
|
||||
#
|
||||
# class Member < ActiveRecord::Base
|
||||
# has_one :avatar
|
||||
# accepts_nested_attributes_for :avatar
|
||||
# end
|
||||
#
|
||||
# Enabling nested attributes on a one-to-one association allows you to
|
||||
# create the member and avatar in one go:
|
||||
#
|
||||
# params = { :member => { :name => 'Jack', :avatar_attributes => { :icon => 'smiling' } } }
|
||||
# member = Member.create(params)
|
||||
# member.avatar.id # => 2
|
||||
# member.avatar.icon # => 'smiling'
|
||||
#
|
||||
# It also allows you to update the avatar through the member:
|
||||
#
|
||||
# params = { :member' => { :avatar_attributes => { :id => '2', :icon => 'sad' } } }
|
||||
# member.update_attributes params['member']
|
||||
# member.avatar.icon # => 'sad'
|
||||
#
|
||||
# By default you will only be able to set and update attributes on the
|
||||
# associated model. If you want to destroy the associated model through the
|
||||
# attributes hash, you have to enable it first using the
|
||||
# <tt>:allow_destroy</tt> option.
|
||||
#
|
||||
# class Member < ActiveRecord::Base
|
||||
# has_one :avatar
|
||||
# accepts_nested_attributes_for :avatar, :allow_destroy => true
|
||||
# end
|
||||
#
|
||||
# Now, when you add the <tt>_destroy</tt> key to the attributes hash, with a
|
||||
# value that evaluates to +true+, you will destroy the associated model:
|
||||
#
|
||||
# member.avatar_attributes = { :id => '2', :_destroy => '1' }
|
||||
# member.avatar.marked_for_destruction? # => true
|
||||
# member.save
|
||||
# member.avatar #=> nil
|
||||
#
|
||||
# Note that the model will _not_ be destroyed until the parent is saved.
|
||||
#
|
||||
# === One-to-many
|
||||
#
|
||||
# Consider a member that has a number of posts:
|
||||
#
|
||||
# class Member < ActiveRecord::Base
|
||||
# has_many :posts
|
||||
# accepts_nested_attributes_for :posts
|
||||
# end
|
||||
#
|
||||
# You can now set or update attributes on an associated post model through
|
||||
# the attribute hash.
|
||||
#
|
||||
# For each hash that does _not_ have an <tt>id</tt> key a new record will
|
||||
# be instantiated, unless the hash also contains a <tt>_destroy</tt> key
|
||||
# that evaluates to +true+.
|
||||
#
|
||||
# params = { :member => {
|
||||
# :name => 'joe', :posts_attributes => [
|
||||
# { :title => 'Kari, the awesome Ruby documentation browser!' },
|
||||
# { :title => 'The egalitarian assumption of the modern citizen' },
|
||||
# { :title => '', :_destroy => '1' } # this will be ignored
|
||||
# ]
|
||||
# }}
|
||||
#
|
||||
# member = Member.create(params['member'])
|
||||
# member.posts.length # => 2
|
||||
# member.posts.first.title # => 'Kari, the awesome Ruby documentation browser!'
|
||||
# member.posts.second.title # => 'The egalitarian assumption of the modern citizen'
|
||||
#
|
||||
# You may also set a :reject_if proc to silently ignore any new record
|
||||
# hashes if they fail to pass your criteria. For example, the previous
|
||||
# example could be rewritten as:
|
||||
#
|
||||
# class Member < ActiveRecord::Base
|
||||
# has_many :posts
|
||||
# accepts_nested_attributes_for :posts, :reject_if => proc { |attributes| attributes['title'].blank? }
|
||||
# end
|
||||
#
|
||||
# params = { :member => {
|
||||
# :name => 'joe', :posts_attributes => [
|
||||
# { :title => 'Kari, the awesome Ruby documentation browser!' },
|
||||
# { :title => 'The egalitarian assumption of the modern citizen' },
|
||||
# { :title => '' } # this will be ignored because of the :reject_if proc
|
||||
# ]
|
||||
# }}
|
||||
#
|
||||
# member = Member.create(params['member'])
|
||||
# member.posts.length # => 2
|
||||
# member.posts.first.title # => 'Kari, the awesome Ruby documentation browser!'
|
||||
# member.posts.second.title # => 'The egalitarian assumption of the modern citizen'
|
||||
#
|
||||
# Alternatively, :reject_if also accepts a symbol for using methods:
|
||||
#
|
||||
# class Member < ActiveRecord::Base
|
||||
# has_many :posts
|
||||
# accepts_nested_attributes_for :posts, :reject_if => :new_record?
|
||||
# end
|
||||
#
|
||||
# class Member < ActiveRecord::Base
|
||||
# has_many :posts
|
||||
# accepts_nested_attributes_for :posts, :reject_if => :reject_posts
|
||||
#
|
||||
# def reject_posts(attributed)
|
||||
# attributed['title].blank?
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# If the hash contains an <tt>id</tt> key that matches an already
|
||||
# associated record, the matching record will be modified:
|
||||
#
|
||||
# member.attributes = {
|
||||
# :name => 'Joe',
|
||||
# :posts_attributes => [
|
||||
# { :id => 1, :title => '[UPDATED] An, as of yet, undisclosed awesome Ruby documentation browser!' },
|
||||
# { :id => 2, :title => '[UPDATED] other post' }
|
||||
# ]
|
||||
# }
|
||||
#
|
||||
# member.posts.first.title # => '[UPDATED] An, as of yet, undisclosed awesome Ruby documentation browser!'
|
||||
# member.posts.second.title # => '[UPDATED] other post'
|
||||
#
|
||||
# By default the associated records are protected from being destroyed. If
|
||||
# you want to destroy any of the associated records through the attributes
|
||||
# hash, you have to enable it first using the <tt>:allow_destroy</tt>
|
||||
# option. This will allow you to also use the <tt>_destroy</tt> key to
|
||||
# destroy existing records:
|
||||
#
|
||||
# class Member < ActiveRecord::Base
|
||||
# has_many :posts
|
||||
# accepts_nested_attributes_for :posts, :allow_destroy => true
|
||||
# end
|
||||
#
|
||||
# params = { :member => {
|
||||
# :posts_attributes => [{ :id => '2', :_destroy => '1' }]
|
||||
# }}
|
||||
#
|
||||
# member.attributes = params['member']
|
||||
# member.posts.detect { |p| p.id == 2 }.marked_for_destruction? # => true
|
||||
# member.posts.length #=> 2
|
||||
# member.save
|
||||
# member.posts.length # => 1
|
||||
#
|
||||
# === Saving
|
||||
#
|
||||
# All changes to models, including the destruction of those marked for
|
||||
# destruction, are saved and destroyed automatically and atomically when
|
||||
# the parent model is saved. This happens inside the transaction initiated
|
||||
# by the parents save method. See ActiveRecord::AutosaveAssociation.
|
||||
module ClassMethods
|
||||
REJECT_ALL_BLANK_PROC = proc { |attributes| attributes.all? { |_, value| value.blank? } }
|
||||
|
||||
# Defines an attributes writer for the specified association(s). If you
|
||||
# are using <tt>attr_protected</tt> or <tt>attr_accessible</tt>, then you
|
||||
# will need to add the attribute writer to the allowed list.
|
||||
#
|
||||
# Supported options:
|
||||
# [:allow_destroy]
|
||||
# If true, destroys any members from the attributes hash with a
|
||||
# <tt>_destroy</tt> key and a value that evaluates to +true+
|
||||
# (eg. 1, '1', true, or 'true'). This option is off by default.
|
||||
# [:reject_if]
|
||||
# Allows you to specify a Proc or a Symbol pointing to a method
|
||||
# that checks whether a record should be built for a certain attribute
|
||||
# hash. The hash is passed to the supplied Proc or the method
|
||||
# and it should return either +true+ or +false+. When no :reject_if
|
||||
# is specified, a record will be built for all attribute hashes that
|
||||
# do not have a <tt>_destroy</tt> value that evaluates to true.
|
||||
# Passing <tt>:all_blank</tt> instead of a Proc will create a proc
|
||||
# that will reject a record where all the attributes are blank.
|
||||
# [:limit]
|
||||
# Allows you to specify the maximum number of the associated records that
|
||||
# can be processes with the nested attributes. If the size of the
|
||||
# nested attributes array exceeds the specified limit, NestedAttributes::TooManyRecords
|
||||
# exception is raised. If omitted, any number associations can be processed.
|
||||
# Note that the :limit option is only applicable to one-to-many associations.
|
||||
# [:update_only]
|
||||
# Allows you to specify that an existing record may only be updated.
|
||||
# A new record may only be created when there is no existing record.
|
||||
# This option only works for one-to-one associations and is ignored for
|
||||
# collection associations. This option is off by default.
|
||||
#
|
||||
# Examples:
|
||||
# # creates avatar_attributes=
|
||||
# accepts_nested_attributes_for :avatar, :reject_if => proc { |attributes| attributes['name'].blank? }
|
||||
# # creates avatar_attributes=
|
||||
# accepts_nested_attributes_for :avatar, :reject_if => :all_blank
|
||||
# # creates avatar_attributes= and posts_attributes=
|
||||
# accepts_nested_attributes_for :avatar, :posts, :allow_destroy => true
|
||||
def accepts_nested_attributes_for(*attr_names)
|
||||
options = { :allow_destroy => false, :update_only => false }
|
||||
options.update(attr_names.extract_options!)
|
||||
options.assert_valid_keys(:allow_destroy, :reject_if, :limit, :update_only)
|
||||
options[:reject_if] = REJECT_ALL_BLANK_PROC if options[:reject_if] == :all_blank
|
||||
|
||||
attr_names.each do |association_name|
|
||||
if reflection = reflect_on_association(association_name)
|
||||
reflection.options[:autosave] = true
|
||||
add_autosave_association_callbacks(reflection)
|
||||
nested_attributes_options[association_name.to_sym] = options
|
||||
type = (reflection.collection? ? :collection : :one_to_one)
|
||||
|
||||
# def pirate_attributes=(attributes)
|
||||
# assign_nested_attributes_for_one_to_one_association(:pirate, attributes)
|
||||
# end
|
||||
class_eval <<-EOS, __FILE__, __LINE__ + 1
|
||||
def #{association_name}_attributes=(attributes)
|
||||
assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes)
|
||||
end
|
||||
EOS
|
||||
else
|
||||
raise ArgumentError, "No association found for name `#{association_name}'. Has it been defined yet?"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Returns ActiveRecord::AutosaveAssociation::marked_for_destruction? It's
|
||||
# used in conjunction with fields_for to build a form element for the
|
||||
# destruction of this association.
|
||||
#
|
||||
# See ActionView::Helpers::FormHelper::fields_for for more info.
|
||||
def _destroy
|
||||
marked_for_destruction?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Attribute hash keys that should not be assigned as normal attributes.
|
||||
# These hash keys are nested attributes implementation details.
|
||||
UNASSIGNABLE_KEYS = %w( id _destroy )
|
||||
|
||||
# Assigns the given attributes to the association.
|
||||
#
|
||||
# If update_only is false and the given attributes include an <tt>:id</tt>
|
||||
# that matches the existing record’s id, then the existing record will be
|
||||
# modified. If update_only is true, a new record is only created when no
|
||||
# object exists. Otherwise a new record will be built.
|
||||
#
|
||||
# If the given attributes include a matching <tt>:id</tt> attribute, or
|
||||
# update_only is true, and a <tt>:_destroy</tt> key set to a truthy value,
|
||||
# then the existing record will be marked for destruction.
|
||||
def assign_nested_attributes_for_one_to_one_association(association_name, attributes)
|
||||
options = nested_attributes_options[association_name]
|
||||
attributes = attributes.with_indifferent_access
|
||||
check_existing_record = (options[:update_only] || !attributes['id'].blank?)
|
||||
|
||||
if check_existing_record && (record = send(association_name)) &&
|
||||
(options[:update_only] || record.id.to_s == attributes['id'].to_s)
|
||||
assign_to_or_mark_for_destruction(record, attributes, options[:allow_destroy])
|
||||
|
||||
elsif attributes['id']
|
||||
existing_record = self.class.reflect_on_association(association_name).klass.find(attributes['id'])
|
||||
assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy])
|
||||
self.send(association_name.to_s+'=', existing_record)
|
||||
|
||||
elsif !reject_new_record?(association_name, attributes)
|
||||
method = "build_#{association_name}"
|
||||
if respond_to?(method)
|
||||
send(method, attributes.except(*UNASSIGNABLE_KEYS))
|
||||
else
|
||||
raise ArgumentError, "Cannot build association #{association_name}. Are you trying to build a polymorphic one-to-one association?"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Assigns the given attributes to the collection association.
|
||||
#
|
||||
# Hashes with an <tt>:id</tt> value matching an existing associated record
|
||||
# will update that record. Hashes without an <tt>:id</tt> value will build
|
||||
# a new record for the association. Hashes with a matching <tt>:id</tt>
|
||||
# value and a <tt>:_destroy</tt> key set to a truthy value will mark the
|
||||
# matched record for destruction.
|
||||
#
|
||||
# For example:
|
||||
#
|
||||
# assign_nested_attributes_for_collection_association(:people, {
|
||||
# '1' => { :id => '1', :name => 'Peter' },
|
||||
# '2' => { :name => 'John' },
|
||||
# '3' => { :id => '2', :_destroy => true }
|
||||
# })
|
||||
#
|
||||
# Will update the name of the Person with ID 1, build a new associated
|
||||
# person with the name `John', and mark the associatied Person with ID 2
|
||||
# for destruction.
|
||||
#
|
||||
# Also accepts an Array of attribute hashes:
|
||||
#
|
||||
# assign_nested_attributes_for_collection_association(:people, [
|
||||
# { :id => '1', :name => 'Peter' },
|
||||
# { :name => 'John' },
|
||||
# { :id => '2', :_destroy => true }
|
||||
# ])
|
||||
def assign_nested_attributes_for_collection_association(association_name, attributes_collection)
|
||||
options = nested_attributes_options[association_name]
|
||||
|
||||
unless attributes_collection.is_a?(Hash) || attributes_collection.is_a?(Array)
|
||||
raise ArgumentError, "Hash or Array expected, got #{attributes_collection.class.name} (#{attributes_collection.inspect})"
|
||||
end
|
||||
|
||||
if options[:limit] && attributes_collection.size > options[:limit]
|
||||
raise TooManyRecords, "Maximum #{options[:limit]} records are allowed. Got #{attributes_collection.size} records instead."
|
||||
end
|
||||
|
||||
if attributes_collection.is_a? Hash
|
||||
attributes_collection = attributes_collection.sort_by { |index, _| index.to_i }.map { |_, attributes| attributes }
|
||||
end
|
||||
|
||||
association = send(association_name)
|
||||
|
||||
existing_records = if association.loaded?
|
||||
association.to_a
|
||||
else
|
||||
attribute_ids = attributes_collection.map {|a| a['id'] || a[:id] }.compact
|
||||
attribute_ids.present? ? association.all(:conditions => {association.primary_key => attribute_ids}) : []
|
||||
end
|
||||
|
||||
attributes_collection.each do |attributes|
|
||||
attributes = attributes.with_indifferent_access
|
||||
|
||||
if attributes['id'].blank?
|
||||
unless reject_new_record?(association_name, attributes)
|
||||
association.build(attributes.except(*UNASSIGNABLE_KEYS))
|
||||
end
|
||||
|
||||
elsif existing_records.size == 0 # Existing record but not yet associated
|
||||
existing_record = self.class.reflect_on_association(association_name).klass.find(attributes['id'])
|
||||
association.send(:add_record_to_target_with_callbacks, existing_record) unless association.loaded?
|
||||
assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy])
|
||||
|
||||
elsif existing_record = existing_records.detect { |record| record.id.to_s == attributes['id'].to_s }
|
||||
association.send(:add_record_to_target_with_callbacks, existing_record) unless association.loaded?
|
||||
assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy])
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Updates a record with the +attributes+ or marks it for destruction if
|
||||
# +allow_destroy+ is +true+ and has_destroy_flag? returns +true+.
|
||||
def assign_to_or_mark_for_destruction(record, attributes, allow_destroy)
|
||||
if has_destroy_flag?(attributes) && allow_destroy
|
||||
record.mark_for_destruction
|
||||
else
|
||||
record.attributes = attributes.except(*UNASSIGNABLE_KEYS)
|
||||
end
|
||||
end
|
||||
|
||||
# Determines if a hash contains a truthy _destroy key.
|
||||
def has_destroy_flag?(hash)
|
||||
ConnectionAdapters::Column.value_to_boolean(hash['_destroy'])
|
||||
end
|
||||
|
||||
# Determines if a new record should be built by checking for
|
||||
# has_destroy_flag? or if a <tt>:reject_if</tt> proc exists for this
|
||||
# association and evaluates to +true+.
|
||||
def reject_new_record?(association_name, attributes)
|
||||
has_destroy_flag?(attributes) || call_reject_if(association_name, attributes)
|
||||
end
|
||||
|
||||
def call_reject_if(association_name, attributes)
|
||||
case callback = nested_attributes_options[association_name][:reject_if]
|
||||
when Symbol
|
||||
method(callback).arity == 0 ? send(callback) : send(callback, attributes)
|
||||
when Proc
|
||||
callback.call(attributes)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
|
@ -1,197 +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)
|
||||
if respond_to?(:after_find) && !klass.method_defined?(:after_find)
|
||||
klass.class_eval 'def after_find() end'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
module ActiveRecord
|
||||
class QueryCache
|
||||
module ClassMethods
|
||||
# Enable the query cache within the block if Active Record is configured.
|
||||
def cache(&block)
|
||||
if ActiveRecord::Base.configurations.blank?
|
||||
yield
|
||||
else
|
||||
connection.cache(&block)
|
||||
end
|
||||
end
|
||||
|
||||
# Disable the query cache within the block if Active Record is configured.
|
||||
def uncached(&block)
|
||||
if ActiveRecord::Base.configurations.blank?
|
||||
yield
|
||||
else
|
||||
connection.uncached(&block)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(app)
|
||||
@app = app
|
||||
end
|
||||
|
||||
def call(env)
|
||||
ActiveRecord::Base.cache do
|
||||
@app.call(env)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,385 +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
|
||||
klass = options[:through] ? ThroughReflection : AssociationReflection
|
||||
reflection = klass.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
|
||||
|
||||
# Returns an array of AssociationReflection objects for all associations which have <tt>:autosave</tt> enabled.
|
||||
def reflect_on_all_autosave_associations
|
||||
reflections.values.select { |reflection| reflection.options[:autosave] }
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# 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)
|
||||
other_aggregation.kind_of?(self.class) && name == other_aggregation.name && other_aggregation.options && active_record == other_aggregation.active_record
|
||||
end
|
||||
|
||||
def sanitized_conditions #:nodoc:
|
||||
@sanitized_conditions ||= klass.send(:sanitize_sql, options[:conditions]) if options[:conditions]
|
||||
end
|
||||
|
||||
# Returns +true+ if +self+ is a +belongs_to+ reflection.
|
||||
def belongs_to?
|
||||
macro == :belongs_to
|
||||
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:
|
||||
# Returns the target association's class:
|
||||
#
|
||||
# class Author < ActiveRecord::Base
|
||||
# has_many :books
|
||||
# end
|
||||
#
|
||||
# Author.reflect_on_association(:books).klass
|
||||
# # => Book
|
||||
#
|
||||
# <b>Note:</b> do not call +klass.new+ or +klass.create+ to instantiate
|
||||
# a new association object. Use +build_association+ or +create_association+
|
||||
# instead. This allows plugins to hook into association object creation.
|
||||
def klass
|
||||
@klass ||= active_record.send(:compute_type, class_name)
|
||||
end
|
||||
|
||||
# Returns a new, unsaved instance of the associated class. +options+ will
|
||||
# be passed to the class's constructor.
|
||||
def build_association(*options)
|
||||
klass.new(*options)
|
||||
end
|
||||
|
||||
# Creates a new instance of the associated class, and immediates saves it
|
||||
# with ActiveRecord::Base#save. +options+ will be passed to the class's
|
||||
# creation method. Returns the newly created object.
|
||||
def create_association(*options)
|
||||
klass.create(*options)
|
||||
end
|
||||
|
||||
# Creates a new instance of the associated class, and immediates saves it
|
||||
# with ActiveRecord::Base#save!. +options+ will be passed to the class's
|
||||
# creation method. If the created record doesn't pass validations, then an
|
||||
# exception will be raised.
|
||||
#
|
||||
# Returns the newly created object.
|
||||
def create_association!(*options)
|
||||
klass.create!(*options)
|
||||
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.demodulize.underscore.pluralize}_count"
|
||||
elsif options[:counter_cache]
|
||||
options[:counter_cache]
|
||||
end
|
||||
end
|
||||
|
||||
def columns(tbl_name, log_msg)
|
||||
@columns ||= klass.connection.columns(tbl_name, log_msg)
|
||||
end
|
||||
|
||||
def reset_column_information
|
||||
@columns = nil
|
||||
end
|
||||
|
||||
def check_validity!
|
||||
check_validity_of_inverse!
|
||||
end
|
||||
|
||||
def check_validity_of_inverse!
|
||||
unless options[:polymorphic]
|
||||
if has_inverse? && inverse_of.nil?
|
||||
raise InverseOfAssociationNotFoundError.new(self)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def through_reflection
|
||||
false
|
||||
end
|
||||
|
||||
def through_reflection_primary_key_name
|
||||
end
|
||||
|
||||
def source_reflection
|
||||
nil
|
||||
end
|
||||
|
||||
def has_inverse?
|
||||
!@options[:inverse_of].nil?
|
||||
end
|
||||
|
||||
def inverse_of
|
||||
if has_inverse?
|
||||
@inverse_of ||= klass.reflect_on_association(options[:inverse_of])
|
||||
end
|
||||
end
|
||||
|
||||
def polymorphic_inverse_of(associated_class)
|
||||
if has_inverse?
|
||||
if inverse_relationship = associated_class.reflect_on_association(options[:inverse_of])
|
||||
inverse_relationship
|
||||
else
|
||||
raise InverseOfAssociationNotFoundError.new(self, associated_class)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Returns whether or not this association reflection is for a collection
|
||||
# association. Returns +true+ if the +macro+ is one of +has_many+ or
|
||||
# +has_and_belongs_to_many+, +false+ otherwise.
|
||||
def collection?
|
||||
if @collection.nil?
|
||||
@collection = [:has_many, :has_and_belongs_to_many].include?(macro)
|
||||
end
|
||||
@collection
|
||||
end
|
||||
|
||||
# Returns whether or not the association should be validated as part of
|
||||
# the parent's validation.
|
||||
#
|
||||
# Unless you explicitely disable validation with
|
||||
# <tt>:validate => false</tt>, it will take place when:
|
||||
#
|
||||
# * you explicitely enable validation; <tt>:validate => true</tt>
|
||||
# * you use autosave; <tt>:autosave => true</tt>
|
||||
# * the association is a +has_many+ association
|
||||
def validate?
|
||||
!options[:validate].nil? ? options[:validate] : (options[:autosave] == true || macro == :has_many)
|
||||
end
|
||||
|
||||
def dependent_conditions(record, base_class, extra_conditions)
|
||||
dependent_conditions = []
|
||||
dependent_conditions << "#{primary_key_name} = #{record.send(name).send(:owner_quoted_id)}"
|
||||
dependent_conditions << "#{options[:as]}_type = '#{base_class.name}'" if options[:as]
|
||||
dependent_conditions << klass.send(:sanitize_sql, options[:conditions]) if options[:conditions]
|
||||
dependent_conditions = dependent_conditions.collect {|where| "(#{where})" }.join(" AND ")
|
||||
dependent_conditions << extra_conditions if extra_conditions
|
||||
dependent_conditions = dependent_conditions.gsub('@', '\@')
|
||||
dependent_conditions
|
||||
end
|
||||
|
||||
private
|
||||
def derive_class_name
|
||||
class_name = name.to_s.camelize
|
||||
class_name = class_name.singularize if collection?
|
||||
class_name
|
||||
end
|
||||
|
||||
def derive_primary_key_name
|
||||
if belongs_to?
|
||||
"#{name}_id"
|
||||
elsif options[:as]
|
||||
"#{options[:as]}_id"
|
||||
else
|
||||
active_record.name.foreign_key
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Holds all the meta-data about a :through association as it was specified in the Active Record class.
|
||||
class ThroughReflection < AssociationReflection #:nodoc:
|
||||
# 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
|
||||
@source_reflection ||= source_reflection_names.collect { |name| through_reflection.klass.reflect_on_association(name) }.compact.first
|
||||
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 ||= active_record.reflect_on_association(options[:through])
|
||||
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
|
||||
|
||||
def check_validity!
|
||||
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, :has_one].include?(source_reflection.macro) && source_reflection.options[:through].nil?
|
||||
raise HasManyThroughSourceAssociationMacroError.new(self)
|
||||
end
|
||||
|
||||
check_validity_of_inverse!
|
||||
end
|
||||
|
||||
def through_reflection_primary_key
|
||||
through_reflection.belongs_to? ? through_reflection.klass.primary_key : through_reflection.primary_key_name
|
||||
end
|
||||
|
||||
def through_reflection_primary_key_name
|
||||
through_reflection.primary_key_name if through_reflection.belongs_to?
|
||||
end
|
||||
|
||||
private
|
||||
def derive_class_name
|
||||
# get the class_name of the belongs_to association of the through reflection
|
||||
options[:source_type] || source_reflection.class_name
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,55 +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
|
||||
|
||||
def self.migrations_path
|
||||
ActiveRecord::Migrator.migrations_path
|
||||
end
|
||||
|
||||
# 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], migrations_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,185 +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
|
||||
|
||||
##
|
||||
# :singleton-method:
|
||||
# A list of tables which should not be dumped to the schema.
|
||||
# Acceptable values are strings as well as regexp.
|
||||
# This setting is only used if ActiveRecord::Base.schema_format == :ruby
|
||||
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
|
||||
|
||||
# first dump primary key column
|
||||
if @connection.respond_to?(:pk_and_sequence_for)
|
||||
pk, pk_seq = @connection.pk_and_sequence_for(table)
|
||||
elsif @connection.respond_to?(:primary_key)
|
||||
pk = @connection.primary_key(table)
|
||||
end
|
||||
|
||||
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|"
|
||||
|
||||
# then dump all non-primary key columns
|
||||
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.has_default?
|
||||
(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)
|
||||
if (indexes = @connection.indexes(table)).any?
|
||||
add_index_statements = indexes.map do |index|
|
||||
statment_parts = [ ('add_index ' + index.table.inspect) ]
|
||||
statment_parts << index.columns.inspect
|
||||
statment_parts << (':name => ' + index.name.inspect)
|
||||
statment_parts << ':unique => true' if index.unique
|
||||
|
||||
index_lengths = index.lengths.compact if index.lengths.is_a?(Array)
|
||||
statment_parts << (':length => ' + Hash[*index.columns.zip(index.lengths).flatten].inspect) if index_lengths.present?
|
||||
|
||||
' ' + statment_parts.join(', ')
|
||||
end
|
||||
|
||||
stream.puts add_index_statements.sort.join("\n")
|
||||
stream.puts
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,101 +0,0 @@
|
|||
require 'active_support/json'
|
||||
|
||||
module ActiveRecord #:nodoc:
|
||||
module Serialization
|
||||
class Serializer #:nodoc:
|
||||
attr_reader :options
|
||||
|
||||
def initialize(record, options = nil)
|
||||
@record = record
|
||||
@options = options ? 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
|
||||
{}.tap do |serializable_record|
|
||||
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,91 +0,0 @@
|
|||
require 'active_support/json'
|
||||
require 'active_support/core_ext/module/model_naming'
|
||||
|
||||
module ActiveRecord #:nodoc:
|
||||
module Serialization
|
||||
def self.included(base)
|
||||
base.cattr_accessor :include_root_in_json, :instance_writer => false
|
||||
end
|
||||
|
||||
# Returns a JSON string representing the model. Some configuration is
|
||||
# available through +options+.
|
||||
#
|
||||
# The option <tt>ActiveRecord::Base.include_root_in_json</tt> controls the
|
||||
# top-level behavior of to_json. In a new Rails application, it is set to
|
||||
# <tt>true</tt> in initializers/new_rails_defaults.rb. When it is <tt>true</tt>,
|
||||
# to_json will emit a single root node named after the object's type. For example:
|
||||
#
|
||||
# konata = User.find(1)
|
||||
# ActiveRecord::Base.include_root_in_json = true
|
||||
# konata.to_json
|
||||
# # => { "user": {"id": 1, "name": "Konata Izumi", "age": 16,
|
||||
# "created_at": "2006/08/01", "awesome": true} }
|
||||
#
|
||||
# ActiveRecord::Base.include_root_in_json = false
|
||||
# konata.to_json
|
||||
# # => {"id": 1, "name": "Konata Izumi", "age": 16,
|
||||
# "created_at": "2006/08/01", "awesome": true}
|
||||
#
|
||||
# The remainder of the examples in this section assume include_root_in_json is set to
|
||||
# <tt>false</tt>.
|
||||
#
|
||||
# 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 = {})
|
||||
super
|
||||
end
|
||||
|
||||
def as_json(options = nil) #:nodoc:
|
||||
hash = Serializer.new(self, options).serializable_record
|
||||
hash = { options[:root] || self.class.model_name.element => hash } if include_root_in_json
|
||||
hash
|
||||
end
|
||||
|
||||
def from_json(json)
|
||||
self.attributes = ActiveSupport::JSON.decode(json)
|
||||
self
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,357 +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>, <tt>:dasherize</tt> and <tt>:camelize</tt> .
|
||||
# The <tt>:only</tt> and <tt>:except</tt> options are the same as for the
|
||||
# +attributes+ method. The default is to dasherize all column names, but you
|
||||
# can disable this setting <tt>:dasherize</tt> to +false+. Setting <tt>:camelize</tt>
|
||||
# to +true+ will camelize all column names - this also overrides <tt>:dasherize</tt>.
|
||||
# To not have the column type included in the XML output set <tt>:skip_types</tt> to +true+.
|
||||
#
|
||||
# For instance:
|
||||
#
|
||||
# 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.model_name.singular).to_s
|
||||
reformat_name(root)
|
||||
end
|
||||
|
||||
def dasherize?
|
||||
!options.has_key?(:dasherize) || options[:dasherize]
|
||||
end
|
||||
|
||||
def camelize?
|
||||
options.has_key?(:camelize) && options[:camelize]
|
||||
end
|
||||
|
||||
def reformat_name(name)
|
||||
name = name.camelize if camelize?
|
||||
dasherize? ? name.dasherize : name
|
||||
end
|
||||
|
||||
def serializable_attributes
|
||||
serializable_attribute_names.collect { |name| Attribute.new(name, @record) }
|
||||
end
|
||||
|
||||
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!(
|
||||
reformat_name(attribute.name),
|
||||
attribute.value.to_s,
|
||||
attribute.decorations(!options[:skip_types])
|
||||
)
|
||||
end
|
||||
|
||||
def add_associations(association, records, opts)
|
||||
if records.is_a?(Enumerable)
|
||||
tag = reformat_name(association.to_s)
|
||||
type = options[:skip_types] ? {} : {:type => "array"}
|
||||
|
||||
if records.empty?
|
||||
builder.tag!(tag, type)
|
||||
else
|
||||
builder.tag!(tag, type) do
|
||||
association_name = association.to_s.singularize
|
||||
records.each do |record|
|
||||
if options[:skip_types]
|
||||
record_type = {}
|
||||
else
|
||||
record_class = (record.class.to_s.underscore == association_name) ? nil : record.class.name
|
||||
record_type = {:type => record_class}
|
||||
end
|
||||
|
||||
record.to_xml opts.merge(:root => association_name).merge(record_type)
|
||||
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 = if @record.class.serialized_attributes.has_key?(name)
|
||||
:yaml
|
||||
else
|
||||
@record.class.columns_hash[name].try(:type)
|
||||
end
|
||||
|
||||
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,334 +0,0 @@
|
|||
module ActiveRecord
|
||||
# A session store backed by an Active Record class. A default class is
|
||||
# provided, but any object duck-typing to an Active Record Session class
|
||||
# with text +session_id+ and +data+ attributes is sufficient.
|
||||
#
|
||||
# The default assumes a +sessions+ tables with columns:
|
||||
# +id+ (numeric primary key),
|
||||
# +session_id+ (text, or longtext if your session data exceeds 65K), and
|
||||
# +data+ (text or longtext; careful if your session data exceeds 65KB).
|
||||
# The +session_id+ column should always be indexed for speedy lookups.
|
||||
# Session data is marshaled to the +data+ column in Base64 format.
|
||||
# If the data you write is larger than the column's size limit,
|
||||
# ActionController::SessionOverflowError will be raised.
|
||||
#
|
||||
# You may configure the table name, primary key, and data column.
|
||||
# For example, at the end of <tt>config/environment.rb</tt>:
|
||||
# ActiveRecord::SessionStore::Session.table_name = 'legacy_session_table'
|
||||
# ActiveRecord::SessionStore::Session.primary_key = 'session_id'
|
||||
# ActiveRecord::SessionStore::Session.data_column_name = 'legacy_session_data'
|
||||
# Note that setting the primary key to the +session_id+ frees you from
|
||||
# having a separate +id+ column if you don't want it. However, you must
|
||||
# set <tt>session.model.id = session.session_id</tt> by hand! A before filter
|
||||
# on ApplicationController is a good place.
|
||||
#
|
||||
# Since the default class is a simple Active Record, you get timestamps
|
||||
# for free if you add +created_at+ and +updated_at+ datetime columns to
|
||||
# the +sessions+ table, making periodic session expiration a snap.
|
||||
#
|
||||
# You may provide your own session class implementation, whether a
|
||||
# feature-packed Active Record or a bare-metal high-performance SQL
|
||||
# store, by setting
|
||||
# ActiveRecord::SessionStore.session_class = MySessionClass
|
||||
# You must implement these methods:
|
||||
# self.find_by_session_id(session_id)
|
||||
# initialize(hash_of_session_id_and_data)
|
||||
# attr_reader :session_id
|
||||
# attr_accessor :data
|
||||
# save
|
||||
# destroy
|
||||
#
|
||||
# The example SqlBypass class is a generic SQL session store. You may
|
||||
# use it as a basis for high-performance database-specific stores.
|
||||
class SessionStore < ActionController::Session::AbstractStore
|
||||
# The default Active Record class.
|
||||
class Session < ActiveRecord::Base
|
||||
##
|
||||
# :singleton-method:
|
||||
# Customizable data column name. Defaults to 'data'.
|
||||
cattr_accessor :data_column_name
|
||||
self.data_column_name = 'data'
|
||||
|
||||
before_save :marshal_data!
|
||||
before_save :raise_on_session_data_overflow!
|
||||
|
||||
class << self
|
||||
def data_column_size_limit
|
||||
@data_column_size_limit ||= columns_hash[@@data_column_name].limit
|
||||
end
|
||||
|
||||
# Hook to set up sessid compatibility.
|
||||
def find_by_session_id(session_id)
|
||||
setup_sessid_compatibility!
|
||||
find_by_session_id(session_id)
|
||||
end
|
||||
|
||||
def marshal(data)
|
||||
ActiveSupport::Base64.encode64(Marshal.dump(data)) if data
|
||||
end
|
||||
|
||||
def unmarshal(data)
|
||||
Marshal.load(ActiveSupport::Base64.decode64(data)) if data
|
||||
end
|
||||
|
||||
def create_table!
|
||||
connection.execute <<-end_sql
|
||||
CREATE TABLE #{table_name} (
|
||||
id INTEGER PRIMARY KEY,
|
||||
#{connection.quote_column_name('session_id')} TEXT UNIQUE,
|
||||
#{connection.quote_column_name(@@data_column_name)} TEXT(255)
|
||||
)
|
||||
end_sql
|
||||
end
|
||||
|
||||
def drop_table!
|
||||
connection.execute "DROP TABLE #{table_name}"
|
||||
end
|
||||
|
||||
private
|
||||
# Compatibility with tables using sessid instead of session_id.
|
||||
def setup_sessid_compatibility!
|
||||
# Reset column info since it may be stale.
|
||||
reset_column_information
|
||||
if columns_hash['sessid']
|
||||
def self.find_by_session_id(*args)
|
||||
find_by_sessid(*args)
|
||||
end
|
||||
|
||||
define_method(:session_id) { sessid }
|
||||
define_method(:session_id=) { |session_id| self.sessid = session_id }
|
||||
else
|
||||
def self.find_by_session_id(session_id)
|
||||
find :first, :conditions => {:session_id=>session_id}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Lazy-unmarshal session state.
|
||||
def data
|
||||
@data ||= self.class.unmarshal(read_attribute(@@data_column_name)) || {}
|
||||
end
|
||||
|
||||
attr_writer :data
|
||||
|
||||
# Has the session been loaded yet?
|
||||
def loaded?
|
||||
!!@data
|
||||
end
|
||||
|
||||
private
|
||||
def marshal_data!
|
||||
return false if !loaded?
|
||||
write_attribute(@@data_column_name, self.class.marshal(self.data))
|
||||
end
|
||||
|
||||
# Ensures that the data about to be stored in the database is not
|
||||
# larger than the data storage column. Raises
|
||||
# ActionController::SessionOverflowError.
|
||||
def raise_on_session_data_overflow!
|
||||
return false if !loaded?
|
||||
limit = self.class.data_column_size_limit
|
||||
if loaded? and limit and read_attribute(@@data_column_name).size > limit
|
||||
raise ActionController::SessionOverflowError
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# A barebones session store which duck-types with the default session
|
||||
# store but bypasses Active Record and issues SQL directly. This is
|
||||
# an example session model class meant as a basis for your own classes.
|
||||
#
|
||||
# The database connection, table name, and session id and data columns
|
||||
# are configurable class attributes. Marshaling and unmarshaling
|
||||
# are implemented as class methods that you may override. By default,
|
||||
# marshaling data is
|
||||
#
|
||||
# ActiveSupport::Base64.encode64(Marshal.dump(data))
|
||||
#
|
||||
# and unmarshaling data is
|
||||
#
|
||||
# Marshal.load(ActiveSupport::Base64.decode64(data))
|
||||
#
|
||||
# This marshaling behavior is intended to store the widest range of
|
||||
# binary session data in a +text+ column. For higher performance,
|
||||
# store in a +blob+ column instead and forgo the Base64 encoding.
|
||||
class SqlBypass
|
||||
##
|
||||
# :singleton-method:
|
||||
# Use the ActiveRecord::Base.connection by default.
|
||||
cattr_accessor :connection
|
||||
|
||||
##
|
||||
# :singleton-method:
|
||||
# The table name defaults to 'sessions'.
|
||||
cattr_accessor :table_name
|
||||
@@table_name = 'sessions'
|
||||
|
||||
##
|
||||
# :singleton-method:
|
||||
# The session id field defaults to 'session_id'.
|
||||
cattr_accessor :session_id_column
|
||||
@@session_id_column = 'session_id'
|
||||
|
||||
##
|
||||
# :singleton-method:
|
||||
# The data field defaults to 'data'.
|
||||
cattr_accessor :data_column
|
||||
@@data_column = 'data'
|
||||
|
||||
class << self
|
||||
def connection
|
||||
@@connection ||= ActiveRecord::Base.connection
|
||||
end
|
||||
|
||||
# Look up a session by id and unmarshal its data if found.
|
||||
def find_by_session_id(session_id)
|
||||
if record = connection.select_one("SELECT * FROM #{@@table_name} WHERE #{@@session_id_column}=#{connection.quote(session_id)}")
|
||||
new(:session_id => session_id, :marshaled_data => record['data'])
|
||||
end
|
||||
end
|
||||
|
||||
def marshal(data)
|
||||
ActiveSupport::Base64.encode64(Marshal.dump(data)) if data
|
||||
end
|
||||
|
||||
def unmarshal(data)
|
||||
Marshal.load(ActiveSupport::Base64.decode64(data)) if data
|
||||
end
|
||||
|
||||
def create_table!
|
||||
@@connection.execute <<-end_sql
|
||||
CREATE TABLE #{table_name} (
|
||||
id INTEGER PRIMARY KEY,
|
||||
#{@@connection.quote_column_name(session_id_column)} TEXT UNIQUE,
|
||||
#{@@connection.quote_column_name(data_column)} TEXT
|
||||
)
|
||||
end_sql
|
||||
end
|
||||
|
||||
def drop_table!
|
||||
@@connection.execute "DROP TABLE #{table_name}"
|
||||
end
|
||||
end
|
||||
|
||||
attr_reader :session_id
|
||||
attr_writer :data
|
||||
|
||||
# Look for normal and marshaled data, self.find_by_session_id's way of
|
||||
# telling us to postpone unmarshaling until the data is requested.
|
||||
# We need to handle a normal data attribute in case of a new record.
|
||||
def initialize(attributes)
|
||||
@session_id, @data, @marshaled_data = attributes[:session_id], attributes[:data], attributes[:marshaled_data]
|
||||
@new_record = @marshaled_data.nil?
|
||||
end
|
||||
|
||||
def new_record?
|
||||
@new_record
|
||||
end
|
||||
|
||||
# Lazy-unmarshal session state.
|
||||
def data
|
||||
unless @data
|
||||
if @marshaled_data
|
||||
@data, @marshaled_data = self.class.unmarshal(@marshaled_data) || {}, nil
|
||||
else
|
||||
@data = {}
|
||||
end
|
||||
end
|
||||
@data
|
||||
end
|
||||
|
||||
def loaded?
|
||||
!!@data
|
||||
end
|
||||
|
||||
def save
|
||||
return false if !loaded?
|
||||
marshaled_data = self.class.marshal(data)
|
||||
|
||||
if @new_record
|
||||
@new_record = false
|
||||
@@connection.update <<-end_sql, 'Create session'
|
||||
INSERT INTO #{@@table_name} (
|
||||
#{@@connection.quote_column_name(@@session_id_column)},
|
||||
#{@@connection.quote_column_name(@@data_column)} )
|
||||
VALUES (
|
||||
#{@@connection.quote(session_id)},
|
||||
#{@@connection.quote(marshaled_data)} )
|
||||
end_sql
|
||||
else
|
||||
@@connection.update <<-end_sql, 'Update session'
|
||||
UPDATE #{@@table_name}
|
||||
SET #{@@connection.quote_column_name(@@data_column)}=#{@@connection.quote(marshaled_data)}
|
||||
WHERE #{@@connection.quote_column_name(@@session_id_column)}=#{@@connection.quote(session_id)}
|
||||
end_sql
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
unless @new_record
|
||||
@@connection.delete <<-end_sql, 'Destroy session'
|
||||
DELETE FROM #{@@table_name}
|
||||
WHERE #{@@connection.quote_column_name(@@session_id_column)}=#{@@connection.quote(session_id)}
|
||||
end_sql
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# The class used for session storage. Defaults to
|
||||
# ActiveRecord::SessionStore::Session
|
||||
cattr_accessor :session_class
|
||||
self.session_class = Session
|
||||
|
||||
SESSION_RECORD_KEY = 'rack.session.record'.freeze
|
||||
|
||||
private
|
||||
def get_session(env, sid)
|
||||
Base.silence do
|
||||
sid ||= generate_sid
|
||||
session = find_session(sid)
|
||||
env[SESSION_RECORD_KEY] = session
|
||||
[sid, session.data]
|
||||
end
|
||||
end
|
||||
|
||||
def set_session(env, sid, session_data)
|
||||
Base.silence do
|
||||
record = get_session_model(env, sid)
|
||||
record.data = session_data
|
||||
return false unless record.save
|
||||
|
||||
session_data = record.data
|
||||
if session_data && session_data.respond_to?(:each_value)
|
||||
session_data.each_value do |obj|
|
||||
obj.clear_association_cache if obj.respond_to?(:clear_association_cache)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
def destroy(env)
|
||||
if sid = current_session_id(env)
|
||||
Base.silence do
|
||||
get_session_model(env, sid).destroy
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def get_session_model(env, sid)
|
||||
if env[ENV_SESSION_OPTIONS_KEY][:id].nil?
|
||||
env[SESSION_RECORD_KEY] = find_session(sid)
|
||||
else
|
||||
env[SESSION_RECORD_KEY] ||= find_session(sid)
|
||||
end
|
||||
end
|
||||
|
||||
def find_session(id)
|
||||
@@session_class.find_by_session_id(id) ||
|
||||
@@session_class.new(:session_id => id, :data => {})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
require "active_support/test_case"
|
||||
|
||||
module ActiveRecord
|
||||
class TestCase < ActiveSupport::TestCase #:nodoc:
|
||||
def assert_date_from_db(expected, actual, message = nil)
|
||||
# SybaseAdapter doesn't have a separate column type just for dates,
|
||||
# so the time is in the string and incorrectly formatted
|
||||
if 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_sql(*patterns_to_match)
|
||||
$queries_executed = []
|
||||
yield
|
||||
ensure
|
||||
failed_patterns = []
|
||||
patterns_to_match.each do |pattern|
|
||||
failed_patterns << pattern unless $queries_executed.any?{ |sql| pattern === sql }
|
||||
end
|
||||
assert failed_patterns.empty?, "Query pattern(s) #{failed_patterns.map(&:inspect).join(', ')} not found."
|
||||
end
|
||||
|
||||
def assert_queries(num = 1)
|
||||
$queries_executed = []
|
||||
yield
|
||||
ensure
|
||||
%w{ BEGIN COMMIT }.each { |x| $queries_executed.delete(x) }
|
||||
assert_equal num, $queries_executed.size, "#{$queries_executed.size} instead of #{num} queries were executed.#{$queries_executed.size == 0 ? '' : "\nQueries:\n#{$queries_executed.join("\n")}"}"
|
||||
end
|
||||
|
||||
def assert_no_queries(&block)
|
||||
assert_queries(0, &block)
|
||||
end
|
||||
|
||||
def self.use_concurrent_connections
|
||||
setup :connection_allow_concurrency_setup
|
||||
teardown :connection_allow_concurrency_teardown
|
||||
end
|
||||
|
||||
def connection_allow_concurrency_setup
|
||||
@connection = ActiveRecord::Base.remove_connection
|
||||
ActiveRecord::Base.establish_connection(@connection.merge({:allow_concurrency => true}))
|
||||
end
|
||||
|
||||
def connection_allow_concurrency_teardown
|
||||
ActiveRecord::Base.clear_all_connections!
|
||||
ActiveRecord::Base.establish_connection(@connection)
|
||||
end
|
||||
|
||||
def with_kcode(kcode)
|
||||
if RUBY_VERSION < '1.9'
|
||||
orig_kcode, $KCODE = $KCODE, kcode
|
||||
begin
|
||||
yield
|
||||
ensure
|
||||
$KCODE = orig_kcode
|
||||
end
|
||||
else
|
||||
yield
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,71 +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
|
||||
|
||||
# Saves the record with the updated_at/on attributes set to the current time.
|
||||
# If the save fails because of validation errors, an ActiveRecord::RecordInvalid exception is raised.
|
||||
# If an attribute name is passed, that attribute is used for the touch instead of the updated_at/on attributes.
|
||||
#
|
||||
# Examples:
|
||||
#
|
||||
# product.touch # updates updated_at
|
||||
# product.touch(:designed_at) # updates the designed_at attribute
|
||||
def touch(attribute = nil)
|
||||
current_time = current_time_from_proper_timezone
|
||||
|
||||
if attribute
|
||||
write_attribute(attribute, current_time)
|
||||
else
|
||||
write_attribute('updated_at', current_time) if respond_to?(:updated_at)
|
||||
write_attribute('updated_on', current_time) if respond_to?(:updated_on)
|
||||
end
|
||||
|
||||
save!
|
||||
end
|
||||
|
||||
|
||||
private
|
||||
def create_with_timestamps #:nodoc:
|
||||
if record_timestamps
|
||||
current_time = current_time_from_proper_timezone
|
||||
|
||||
write_attribute('created_at', current_time) if respond_to?(:created_at) && created_at.nil?
|
||||
write_attribute('created_on', current_time) if respond_to?(:created_on) && created_on.nil?
|
||||
|
||||
write_attribute('updated_at', current_time) if respond_to?(:updated_at) && updated_at.nil?
|
||||
write_attribute('updated_on', current_time) if respond_to?(:updated_on) && updated_on.nil?
|
||||
end
|
||||
|
||||
create_without_timestamps
|
||||
end
|
||||
|
||||
def update_with_timestamps(*args) #:nodoc:
|
||||
if record_timestamps && (!partial_updates? || changed?)
|
||||
current_time = current_time_from_proper_timezone
|
||||
|
||||
write_attribute('updated_at', current_time) if respond_to?(:updated_at)
|
||||
write_attribute('updated_on', current_time) if respond_to?(:updated_on)
|
||||
end
|
||||
|
||||
update_without_timestamps(*args)
|
||||
end
|
||||
|
||||
def current_time_from_proper_timezone
|
||||
self.class.default_timezone == :utc ? Time.now.utc : Time.now
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,235 +0,0 @@
|
|||
require 'thread'
|
||||
|
||||
module ActiveRecord
|
||||
# See ActiveRecord::Transactions::ClassMethods for documentation.
|
||||
module Transactions
|
||||
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:
|
||||
#
|
||||
# ActiveRecord::Base.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. This is because transactions are per-database connection, not
|
||||
# per-model.
|
||||
#
|
||||
# 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
|
||||
#
|
||||
# Note that the +transaction+ method is also available as a model instance
|
||||
# method. For example, you can also do this:
|
||||
#
|
||||
# balance.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, including <tt>after_*</tt> callbacks.
|
||||
#
|
||||
# == Exception handling and rolling back
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# *Warning*: one should not catch ActiveRecord::StatementInvalid exceptions
|
||||
# inside a transaction block. StatementInvalid exceptions indicate that an
|
||||
# error occurred at the database level, for example when a unique constraint
|
||||
# is violated. On some database systems, such as PostgreSQL, database errors
|
||||
# inside a transaction causes the entire transaction to become unusable
|
||||
# until it's restarted from the beginning. Here is an example which
|
||||
# demonstrates the problem:
|
||||
#
|
||||
# # Suppose that we have a Number model with a unique column called 'i'.
|
||||
# Number.transaction do
|
||||
# Number.create(:i => 0)
|
||||
# begin
|
||||
# # This will raise a unique constraint error...
|
||||
# Number.create(:i => 0)
|
||||
# rescue ActiveRecord::StatementInvalid
|
||||
# # ...which we ignore.
|
||||
# end
|
||||
#
|
||||
# # On PostgreSQL, the transaction is now unusable. The following
|
||||
# # statement will cause a PostgreSQL error, even though the unique
|
||||
# # constraint is no longer violated:
|
||||
# Number.create(:i => 1)
|
||||
# # => "PGError: ERROR: current transaction is aborted, commands
|
||||
# # ignored until end of transaction block"
|
||||
# end
|
||||
#
|
||||
# One should restart the entire transaction if a StatementError occurred.
|
||||
#
|
||||
# == Nested transactions
|
||||
#
|
||||
# #transaction calls can be nested. By default, this makes all database
|
||||
# statements in the nested transaction block become part of the parent
|
||||
# transaction. For example:
|
||||
#
|
||||
# User.transaction do
|
||||
# User.create(:username => 'Kotori')
|
||||
# User.transaction do
|
||||
# User.create(:username => 'Nemu')
|
||||
# raise ActiveRecord::Rollback
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# User.find(:all) # => empty
|
||||
#
|
||||
# It is also possible to requires a sub-transaction by passing
|
||||
# <tt>:requires_new => true</tt>. If anything goes wrong, the
|
||||
# database rolls back to the beginning of the sub-transaction
|
||||
# without rolling back the parent transaction. For example:
|
||||
#
|
||||
# User.transaction do
|
||||
# User.create(:username => 'Kotori')
|
||||
# User.transaction(:requires_new => true) do
|
||||
# User.create(:username => 'Nemu')
|
||||
# raise ActiveRecord::Rollback
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# User.find(:all) # => Returns only Kotori
|
||||
#
|
||||
# Most databases don't support true nested transactions. At the time of
|
||||
# writing, the only database that we're aware of that supports true nested
|
||||
# transactions, is MS-SQL. Because of this, Active Record emulates nested
|
||||
# transactions by using savepoints. See
|
||||
# http://dev.mysql.com/doc/refman/5.0/en/savepoints.html
|
||||
# for more information about savepoints.
|
||||
#
|
||||
# === Caveats
|
||||
#
|
||||
# If you're on MySQL, then do not use DDL operations in nested transactions
|
||||
# blocks that are emulated with savepoints. That is, do not execute statements
|
||||
# like 'CREATE TABLE' inside such blocks. This is because MySQL automatically
|
||||
# releases all savepoints upon executing a DDL operation. When #transaction
|
||||
# is finished and tries to release the savepoint it created earlier, a
|
||||
# database error will occur because the savepoint has already been
|
||||
# automatically released. The following example demonstrates the problem:
|
||||
#
|
||||
# Model.connection.transaction do # BEGIN
|
||||
# Model.connection.transaction(:requires_new => true) do # CREATE SAVEPOINT active_record_1
|
||||
# Model.connection.create_table(...) # active_record_1 now automatically released
|
||||
# end # RELEASE savepoint active_record_1
|
||||
# # ^^^^ BOOM! database error!
|
||||
# end
|
||||
module ClassMethods
|
||||
# See ActiveRecord::Transactions::ClassMethods for detailed documentation.
|
||||
def transaction(options = {}, &block)
|
||||
# See the ConnectionAdapters::DatabaseStatements#transaction API docs.
|
||||
connection.transaction(options, &block)
|
||||
end
|
||||
end
|
||||
|
||||
# See ActiveRecord::Transactions::ClassMethods for detailed documentation.
|
||||
def transaction(&block)
|
||||
self.class.transaction(&block)
|
||||
end
|
||||
|
||||
def destroy_with_transactions #:nodoc:
|
||||
with_transaction_returning_status(:destroy_without_transactions)
|
||||
end
|
||||
|
||||
def save_with_transactions(perform_validation = true) #:nodoc:
|
||||
rollback_active_record_state! { with_transaction_returning_status(:save_without_transactions, perform_validation) }
|
||||
end
|
||||
|
||||
def save_with_transactions! #:nodoc:
|
||||
rollback_active_record_state! { self.class.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
|
||||
|
||||
# Executes +method+ within a transaction and captures its return value as a
|
||||
# status flag. If the status is true the transaction is committed, otherwise
|
||||
# a ROLLBACK is issued. In any case the status flag is returned.
|
||||
#
|
||||
# This method is available within the context of an ActiveRecord::Base
|
||||
# instance.
|
||||
def with_transaction_returning_status(method, *args)
|
||||
status = nil
|
||||
self.class.transaction do
|
||||
status = send(method, *args)
|
||||
raise ActiveRecord::Rollback unless status
|
||||
end
|
||||
status
|
||||
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 = 3
|
||||
TINY = 9
|
||||
|
||||
STRING = [MAJOR, MINOR, TINY].join('.')
|
||||
end
|
||||
end
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
require 'active_record'
|
||||
ActiveSupport::Deprecation.warn 'require "activerecord" is deprecated and will be removed in Rails 3. Use require "active_record" instead.'
|
||||
|
|
@ -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,122 +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_add_index
|
||||
# add_index calls index_exists? which can't work since execute is stubbed
|
||||
ActiveRecord::ConnectionAdapters::MysqlAdapter.send(:define_method, :index_exists?) do |*|
|
||||
false
|
||||
end
|
||||
expected = "CREATE INDEX `index_people_on_last_name` ON `people` (`last_name`)"
|
||||
assert_equal expected, add_index(:people, :last_name, :length => nil)
|
||||
|
||||
expected = "CREATE INDEX `index_people_on_last_name` ON `people` (`last_name`(10))"
|
||||
assert_equal expected, add_index(:people, :last_name, :length => 10)
|
||||
|
||||
expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`(15))"
|
||||
assert_equal expected, add_index(:people, [:last_name, :first_name], :length => 15)
|
||||
|
||||
expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`)"
|
||||
assert_equal expected, add_index(:people, [:last_name, :first_name], :length => {:last_name => 15})
|
||||
|
||||
expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`(10))"
|
||||
assert_equal expected, add_index(:people, [:last_name, :first_name], :length => {:last_name => 15, :first_name => 10})
|
||||
ActiveRecord::ConnectionAdapters::MysqlAdapter.send(:remove_method, :index_exists?)
|
||||
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
|
||||
|
||||
def test_recreate_mysql_database_with_encoding
|
||||
create_database(:luca, {:charset => 'latin1'})
|
||||
assert_equal "CREATE DATABASE `luca` DEFAULT CHARACTER SET `latin1`", recreate_database(:luca, {:charset => 'latin1'})
|
||||
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
|
||||
144
vendor/rails/activerecord/test/cases/adapter_test.rb
vendored
144
vendor/rails/activerecord/test/cases/adapter_test.rb
vendored
|
|
@ -1,144 +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
|
||||
|
||||
def test_not_specifying_database_name_for_cross_database_selects
|
||||
begin
|
||||
assert_nothing_raised do
|
||||
ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['arunit'].except(:database))
|
||||
ActiveRecord::Base.connection.execute "SELECT activerecord_unittest.pirates.*, activerecord_unittest2.courses.* FROM activerecord_unittest.pirates, activerecord_unittest2.courses"
|
||||
end
|
||||
ensure
|
||||
ActiveRecord::Base.establish_connection 'arunit'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if current_adapter?(:PostgreSQLAdapter)
|
||||
def test_encoding
|
||||
assert_not_nil @connection.encoding
|
||||
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=> '1 ; DROP TABLE USERS', :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,167 +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
|
||||
|
||||
def test_custom_constructor
|
||||
assert_equal 'Barney GUMBLE', customers(:barney).fullname.to_s
|
||||
assert_kind_of Fullname, customers(:barney).fullname
|
||||
end
|
||||
|
||||
def test_custom_converter
|
||||
customers(:barney).fullname = 'Barnoit Gumbleau'
|
||||
assert_equal 'Barnoit GUMBLEAU', customers(:barney).fullname.to_s
|
||||
assert_kind_of Fullname, customers(:barney).fullname
|
||||
end
|
||||
end
|
||||
|
||||
class DeprecatedAggregationsTest < ActiveRecord::TestCase
|
||||
class Person < ActiveRecord::Base; end
|
||||
|
||||
def test_conversion_block_is_deprecated
|
||||
assert_deprecated 'conversion block has been deprecated' do
|
||||
Person.composed_of(:balance, :class_name => "Money", :mapping => %w(balance amount)) { |balance| balance.to_money }
|
||||
end
|
||||
end
|
||||
|
||||
def test_conversion_block_used_when_converter_option_is_nil
|
||||
assert_deprecated 'conversion block has been deprecated' do
|
||||
Person.composed_of(:balance, :class_name => "Money", :mapping => %w(balance amount)) { |balance| balance.to_money }
|
||||
end
|
||||
assert_raise(NoMethodError) { Person.new.balance = 5 }
|
||||
end
|
||||
|
||||
def test_converter_option_overrides_conversion_block
|
||||
assert_deprecated 'conversion block has been deprecated' do
|
||||
Person.composed_of(:balance, :class_name => "Money", :mapping => %w(balance amount), :converter => Proc.new { |balance| Money.new(balance) }) { |balance| balance.to_money }
|
||||
end
|
||||
|
||||
person = Person.new
|
||||
assert_nothing_raised { person.balance = 5 }
|
||||
assert_equal 5, person.balance.amount
|
||||
assert_kind_of Money, person.balance
|
||||
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,32 +0,0 @@
|
|||
require "cases/helper"
|
||||
|
||||
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,438 +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'
|
||||
require 'models/essay'
|
||||
|
||||
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_belongs_to_with_primary_key
|
||||
client = Client.create(:name => "Primary key client", :firm_name => companies(:first_firm).name)
|
||||
assert_equal companies(:first_firm).name, client.firm_with_primary_key.name
|
||||
end
|
||||
|
||||
def test_belongs_to_with_primary_key_joins_on_correct_column
|
||||
sql = Client.send(:construct_finder_sql, :joins => :firm_with_primary_key)
|
||||
assert sql !~ /\.id/
|
||||
assert sql =~ /\.name/
|
||||
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_natural_assignment_with_primary_key
|
||||
apple = Firm.create("name" => "Apple")
|
||||
citibank = Client.create("name" => "Primary key client")
|
||||
citibank.firm_with_primary_key = apple
|
||||
assert_equal apple.name, citibank.firm_name
|
||||
end
|
||||
|
||||
def test_eager_loading_with_primary_key
|
||||
apple = Firm.create("name" => "Apple")
|
||||
citibank = Client.create("name" => "Citibank", :firm_name => "Apple")
|
||||
citibank_result = Client.find(:first, :conditions => {:name => "Citibank"}, :include => :firm_with_primary_key)
|
||||
assert_not_nil citibank_result.instance_variable_get("@firm_with_primary_key")
|
||||
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_creating_the_belonging_object_with_primary_key
|
||||
client = Client.create(:name => "Primary key client")
|
||||
apple = client.create_firm_with_primary_key("name" => "Apple")
|
||||
assert_equal apple, client.firm_with_primary_key
|
||||
client.save
|
||||
client.reload
|
||||
assert_equal apple, client.firm_with_primary_key
|
||||
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_building_the_belonging_object_with_primary_key
|
||||
client = Client.create(:name => "Primary key client")
|
||||
apple = client.build_firm_with_primary_key("name" => "Apple")
|
||||
client.save
|
||||
assert_equal apple.name, client.firm_name
|
||||
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_natural_assignment_to_nil_with_primary_key
|
||||
client = Client.create(:name => "Primary key client", :firm_name => companies(:first_firm).name)
|
||||
client.firm_with_primary_key = nil
|
||||
client.save
|
||||
assert_nil client.firm_with_primary_key(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_with_primary_key_counter
|
||||
debate = Topic.create("title" => "debate")
|
||||
assert_equal 0, debate.send(:read_attribute, "replies_count"), "No replies yet"
|
||||
|
||||
trash = debate.replies_with_primary_key.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_with_primary_key_counter_with_assigning_nil
|
||||
debate = Topic.create("title" => "debate")
|
||||
reply = Reply.create("title" => "blah!", "content" => "world around!", "parent_title" => "debate")
|
||||
|
||||
assert_equal debate.title, reply.parent_title
|
||||
assert_equal 1, Topic.find(debate.id).send(:read_attribute, "replies_count")
|
||||
|
||||
reply.topic_with_primary_key = nil
|
||||
|
||||
assert_equal 0, Topic.find(debate.id).send(:read_attribute, "replies_count")
|
||||
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_reassign_with_namespaced_models_and_counters
|
||||
t1 = Web::Topic.create("title" => "t1")
|
||||
t2 = Web::Topic.create("title" => "t2")
|
||||
r1 = Web::Reply.new("title" => "r1", "content" => "r1")
|
||||
r1.topic = t1
|
||||
|
||||
assert r1.save
|
||||
assert_equal 1, Web::Topic.find(t1.id).replies.size
|
||||
assert_equal 0, Web::Topic.find(t2.id).replies.size
|
||||
|
||||
r1.topic = Web::Topic.find(t2.id)
|
||||
|
||||
assert r1.save
|
||||
assert_equal 0, Web::Topic.find(t1.id).replies.size
|
||||
assert_equal 1, Web::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_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_child_saved_with_primary_key
|
||||
final_cut = Client.new("name" => "Final Cut")
|
||||
firm = Firm.find(1)
|
||||
final_cut.firm_with_primary_key = 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_with_primary_key
|
||||
assert_equal firm, final_cut.firm_with_primary_key(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_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_with_primary_key_foreign_type_field_updating
|
||||
# should update when assigning a saved record
|
||||
essay = Essay.new
|
||||
writer = Author.create(:name => "David")
|
||||
essay.writer = writer
|
||||
assert_equal "Author", essay.writer_type
|
||||
|
||||
# should update when assigning a new record
|
||||
essay = Essay.new
|
||||
writer = Author.new
|
||||
essay.writer = writer
|
||||
assert_equal "Author", essay.writer_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
|
||||
|
||||
def test_polymorphic_assignment_with_primary_key_updates_foreign_id_field_for_new_and_saved_records
|
||||
essay = Essay.new
|
||||
saved_writer = Author.create(:name => "David")
|
||||
new_writer = Author.new
|
||||
|
||||
essay.writer = saved_writer
|
||||
assert_equal saved_writer.name, essay.writer_id
|
||||
|
||||
essay.writer = new_writer
|
||||
assert_equal nil, essay.writer_id
|
||||
end
|
||||
|
||||
def test_belongs_to_proxy_should_not_respond_to_private_methods
|
||||
assert_raise(NoMethodError) { companies(:first_firm).private_method }
|
||||
assert_raise(NoMethodError) { companies(:second_client).firm.private_method }
|
||||
end
|
||||
|
||||
def test_belongs_to_proxy_should_respond_to_private_methods_via_send
|
||||
companies(:first_firm).send(:private_method)
|
||||
companies(:second_client).firm.send(:private_method)
|
||||
end
|
||||
|
||||
def test_save_of_record_with_loaded_belongs_to
|
||||
@account = companies(:first_firm).account
|
||||
|
||||
assert_nothing_raised do
|
||||
Account.find(@account.id).save!
|
||||
Account.find(@account.id, :include => :firm).save!
|
||||
end
|
||||
|
||||
@account.firm.delete
|
||||
|
||||
assert_nothing_raised do
|
||||
Account.find(@account.id).save!
|
||||
Account.find(@account.id, :include => :firm).save!
|
||||
end
|
||||
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,131 +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, :accounts, :comments, :categorizations
|
||||
|
||||
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_has_many_sti_and_subclasses
|
||||
silly = SillyReply.new(:title => "gaga", :content => "boo-boo", :parent_id => 1)
|
||||
silly.parent_id = 1
|
||||
assert silly.save
|
||||
|
||||
topics = Topic.find(:all, :include => :replies, :order => 'topics.id, replies_topics.id')
|
||||
assert_no_queries do
|
||||
assert_equal 2, topics[0].replies.size
|
||||
assert_equal 0, 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
|
||||
|
||||
def test_eager_association_loading_where_first_level_returns_nil
|
||||
authors = Author.find(:all, :include => {:post_about_thinking => :comments}, :order => 'authors.id DESC')
|
||||
assert_equal [authors(:mary), authors(:david)], authors
|
||||
assert_no_queries do
|
||||
authors[1].post_about_thinking.comments.first
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
require 'models/vertex'
|
||||
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,36 +0,0 @@
|
|||
require 'cases/helper'
|
||||
require 'models/post'
|
||||
require 'models/tagging'
|
||||
|
||||
module Namespaced
|
||||
class Post < ActiveRecord::Base
|
||||
set_table_name 'posts'
|
||||
has_one :tagging, :as => :taggable, :class_name => 'Tagging'
|
||||
end
|
||||
end
|
||||
|
||||
class EagerLoadIncludeFullStiClassNamesTest < ActiveRecord::TestCase
|
||||
|
||||
def setup
|
||||
generate_test_objects
|
||||
end
|
||||
|
||||
def generate_test_objects
|
||||
post = Namespaced::Post.create( :title => 'Great stuff', :body => 'This is not', :author_id => 1 )
|
||||
tagging = Tagging.create( :taggable => post )
|
||||
end
|
||||
|
||||
def test_class_names
|
||||
old = ActiveRecord::Base.store_full_sti_class
|
||||
|
||||
ActiveRecord::Base.store_full_sti_class = false
|
||||
post = Namespaced::Post.find_by_title( 'Great stuff', :include => :tagging )
|
||||
assert_nil post.tagging
|
||||
|
||||
ActiveRecord::Base.store_full_sti_class = true
|
||||
post = Namespaced::Post.find_by_title( 'Great stuff', :include => :tagging )
|
||||
assert_equal 'Tagging', post.tagging.class.name
|
||||
ensure
|
||||
ActiveRecord::Base.store_full_sti_class = old
|
||||
end
|
||||
end
|
||||
|
|
@ -1,131 +0,0 @@
|
|||
require 'cases/helper'
|
||||
require 'models/post'
|
||||
require 'models/author'
|
||||
require 'models/comment'
|
||||
require 'models/category'
|
||||
require 'models/categorization'
|
||||
require 'active_support/core_ext/array/random_access'
|
||||
|
||||
module Remembered
|
||||
def self.included(base)
|
||||
base.extend ClassMethods
|
||||
base.class_eval do
|
||||
after_create :remember
|
||||
protected
|
||||
def remember; self.class.remembered << self; end
|
||||
end
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
def remembered; @@remembered ||= []; end
|
||||
def sample; @@remembered.sample; end
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
include Remembered
|
||||
end
|
||||
class Square < ActiveRecord::Base
|
||||
has_many :shape_expressions, :as => :shape
|
||||
include Remembered
|
||||
end
|
||||
class Triangle < ActiveRecord::Base
|
||||
has_many :shape_expressions, :as => :shape
|
||||
include Remembered
|
||||
end
|
||||
class PaintColor < ActiveRecord::Base
|
||||
has_many :shape_expressions, :as => :paint
|
||||
belongs_to :non_poly, :foreign_key => "non_poly_one_id", :class_name => "NonPolyOne"
|
||||
include Remembered
|
||||
end
|
||||
class PaintTexture < ActiveRecord::Base
|
||||
has_many :shape_expressions, :as => :paint
|
||||
belongs_to :non_poly, :foreign_key => "non_poly_two_id", :class_name => "NonPolyTwo"
|
||||
include Remembered
|
||||
end
|
||||
class NonPolyOne < ActiveRecord::Base
|
||||
has_many :paint_colors
|
||||
include Remembered
|
||||
end
|
||||
class NonPolyTwo < ActiveRecord::Base
|
||||
has_many :paint_textures
|
||||
include Remembered
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
PaintColor.create!(:non_poly_one_id => NonPolyOne.sample.id)
|
||||
PaintTexture.create!(:non_poly_two_id => NonPolyTwo.sample.id)
|
||||
end
|
||||
1.upto(NUM_SHAPE_EXPRESSIONS) do
|
||||
shape_type = [Circle, Square, Triangle].sample
|
||||
paint_type = [PaintColor, PaintTexture].sample
|
||||
ShapeExpression.create!(:shape_type => shape_type.to_s, :shape_id => shape_type.sample.id,
|
||||
:paint_type => paint_type.to_s, :paint_id => paint_type.sample.id)
|
||||
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
|
||||
|
||||
class EagerLoadNestedIncludeWithMissingDataTest < ActiveRecord::TestCase
|
||||
def setup
|
||||
@davey_mcdave = Author.create(:name => 'Davey McDave')
|
||||
@first_post = @davey_mcdave.posts.create(:title => 'Davey Speaks', :body => 'Expressive wordage')
|
||||
@first_comment = @first_post.comments.create(:body => 'Inflamatory doublespeak')
|
||||
@first_categorization = @davey_mcdave.categorizations.create(:category => Category.first, :post => @first_post)
|
||||
end
|
||||
|
||||
def teardown
|
||||
@davey_mcdave.destroy
|
||||
@first_post.destroy
|
||||
@first_comment.destroy
|
||||
@first_categorization.destroy
|
||||
end
|
||||
|
||||
def test_missing_data_in_a_nested_include_should_not_cause_errors_when_constructing_objects
|
||||
assert_nothing_raised do
|
||||
# @davey_mcdave doesn't have any author_favorites
|
||||
includes = {:posts => :comments, :categorizations => :category, :author_favorites => :favorite_author }
|
||||
Author.all :include => includes, :conditions => {:authors => {:name => @davey_mcdave.name}}, :order => 'categories.name'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
require 'cases/helper'
|
||||
require 'models/tee'
|
||||
require 'models/tie'
|
||||
require 'models/polymorphic_design'
|
||||
require 'models/polymorphic_price'
|
||||
|
||||
class EagerLoadNestedPolymorphicIncludeTest < ActiveRecord::TestCase
|
||||
fixtures :tees, :ties, :polymorphic_designs, :polymorphic_prices
|
||||
|
||||
def test_eager_load_polymorphic_has_one_nested_under_polymorphic_belongs_to
|
||||
designs = PolymorphicDesign.scoped(:include => {:designable => :polymorphic_price})
|
||||
|
||||
associated_price_ids = designs.map{|design| design.designable.polymorphic_price.id}
|
||||
expected_price_ids = [1, 2, 3, 4]
|
||||
|
||||
assert expected_price_ids.all?{|expected_id| associated_price_ids.include?(expected_id)},
|
||||
"Expected associated prices to be #{expected_price_ids.inspect} but they were #{associated_price_ids.sort.inspect}"
|
||||
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,847 +0,0 @@
|
|||
require "cases/helper"
|
||||
require 'models/post'
|
||||
require 'models/tagging'
|
||||
require 'models/tag'
|
||||
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'
|
||||
require 'models/developer'
|
||||
require 'models/project'
|
||||
|
||||
class EagerAssociationTest < ActiveRecord::TestCase
|
||||
fixtures :posts, :comments, :authors, :author_addresses, :categories, :categories_posts,
|
||||
:companies, :accounts, :tags, :taggings, :people, :readers,
|
||||
:owners, :pets, :author_favorites, :jobs, :references, :subscribers, :subscriptions, :books,
|
||||
:developers, :projects, :developers_projects
|
||||
|
||||
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_with_one_association_with_non_preload
|
||||
posts = Post.find(:all, :include => :last_comment, :order => 'comments.id DESC')
|
||||
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_finding_with_includes_on_has_many_association_with_same_include_includes_only_once
|
||||
author_id = authors(:david).id
|
||||
author = assert_queries(3) { Author.find(author_id, :include => {:posts_with_comments => :comments}) } # find the author, then find the posts, then find the comments
|
||||
author.posts_with_comments.each do |post_with_comments|
|
||||
assert_equal post_with_comments.comments.length, post_with_comments.comments.count
|
||||
assert_equal nil, post_with_comments.comments.uniq!
|
||||
end
|
||||
end
|
||||
|
||||
def test_finding_with_includes_on_has_one_assocation_with_same_include_includes_only_once
|
||||
author = authors(:david)
|
||||
post = author.post_about_thinking_with_last_comment
|
||||
last_comment = post.last_comment
|
||||
author = assert_queries(3) { Author.find(author.id, :include => {:post_about_thinking_with_last_comment => :last_comment})} # find the author, then find the posts, then find the comments
|
||||
assert_no_queries do
|
||||
assert_equal post, author.post_about_thinking_with_last_comment
|
||||
assert_equal last_comment, author.post_about_thinking_with_last_comment.last_comment
|
||||
end
|
||||
end
|
||||
|
||||
def test_finding_with_includes_on_belongs_to_association_with_same_include_includes_only_once
|
||||
post = posts(:welcome)
|
||||
author = post.author
|
||||
author_address = author.author_address
|
||||
post = assert_queries(3) { Post.find(post.id, :include => {:author_with_address => :author_address}) } # find the post, then find the author, then find the address
|
||||
assert_no_queries do
|
||||
assert_equal author, post.author_with_address
|
||||
assert_equal author_address, post.author_with_address.author_address
|
||||
end
|
||||
end
|
||||
|
||||
def test_finding_with_includes_on_null_belongs_to_association_with_same_include_includes_only_once
|
||||
post = posts(:welcome)
|
||||
post.update_attributes!(:author => nil)
|
||||
post = assert_queries(1) { Post.find(post.id, :include => {:author_with_address => :author_address}) } # find the post, then find the author which is null so no query for the author or address
|
||||
assert_no_queries do
|
||||
assert_equal nil, post.author_with_address
|
||||
end
|
||||
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_from_an_association_that_has_a_hash_of_conditions
|
||||
assert_nothing_raised do
|
||||
Author.find(:all, :include => :hello_posts_with_hash_conditions)
|
||||
end
|
||||
assert !Author.find(authors(:david).id, :include => :hello_posts_with_hash_conditions).hello_posts.empty?
|
||||
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_hash
|
||||
comments = []
|
||||
assert_nothing_raised do
|
||||
comments = Comment.find(:all, :include => :post, :conditions => {:posts => {:id => 4}}, :limit => 3, :order => 'comments.id')
|
||||
end
|
||||
assert_equal 3, comments.length
|
||||
assert_equal [5,6,7], comments.collect { |c| c.id }
|
||||
assert_no_queries do
|
||||
comments.first.post
|
||||
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, :order => 'posts.id')
|
||||
posts_with_author = people(:michael).posts.find(:all, :include => :author, :order => 'posts.id')
|
||||
posts_with_comments_and_author = people(:michael).posts.find(:all, :include => [ :comments, :author ], :order => 'posts.id')
|
||||
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_a_belongs_to_association
|
||||
author = authors(:mary)
|
||||
post = Post.create!(:author => author, :title => "TITLE", :body => "BODY")
|
||||
author.author_favorites.create(:favorite_author_id => 1)
|
||||
author.author_favorites.create(:favorite_author_id => 2)
|
||||
posts_with_author_favorites = author.posts.find(:all, :include => :author_favorites)
|
||||
assert_no_queries { posts_with_author_favorites.first.author_favorites.first.author_id }
|
||||
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_with_conditions_join_model_with_include
|
||||
post_tags = Post.find(posts(:welcome).id).misc_tags
|
||||
eager_post_tags = Post.find(1, :include => :misc_tags).misc_tags
|
||||
assert_equal post_tags, eager_post_tags
|
||||
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_and_high_offset
|
||||
posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :offset => 10, :conditions => [ "authors.name = ?", 'David' ])
|
||||
assert_equal 0, posts.size
|
||||
end
|
||||
|
||||
def test_eager_with_has_many_and_limit_and_high_offset_and_multiple_array_conditions
|
||||
assert_queries(1) do
|
||||
posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :offset => 10,
|
||||
:conditions => [ "authors.name = ? and comments.body = ?", 'David', 'go crazy' ])
|
||||
assert_equal 0, posts.size
|
||||
end
|
||||
end
|
||||
|
||||
def test_eager_with_has_many_and_limit_and_high_offset_and_multiple_hash_conditions
|
||||
assert_queries(1) do
|
||||
posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :offset => 10,
|
||||
:conditions => { 'authors.name' => 'David', 'comments.body' => 'go crazy' })
|
||||
assert_equal 0, posts.size
|
||||
end
|
||||
end
|
||||
|
||||
def test_count_eager_with_has_many_and_limit_and_high_offset
|
||||
posts = Post.count(:all, :include => [ :author, :comments ], :limit => 2, :offset => 10, :conditions => [ "authors.name = ?", 'David' ])
|
||||
assert_equal 0, posts
|
||||
end
|
||||
|
||||
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_raise(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") {
|
||||
post = Post.find(6, :include=> :monkeys )
|
||||
}
|
||||
assert_raise(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") {
|
||||
post = Post.find(6, :include=>[ :monkeys ])
|
||||
}
|
||||
assert_raise(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") {
|
||||
post = Post.find(6, :include=>[ 'monkeys' ])
|
||||
}
|
||||
assert_raise(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_eager_with_floating_point_numbers
|
||||
assert_queries(2) do
|
||||
# Before changes, the floating point numbers will be interpreted as table names and will cause this to run in one query
|
||||
Comment.find :all, :conditions => "123.456 = 123.456", :include => :post
|
||||
end
|
||||
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?(: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
|
||||
|
||||
def test_conditions_on_join_table_with_include_and_limit
|
||||
assert_equal 3, Developer.find(:all, :include => 'projects', :conditions => 'developers_projects.access_level = 1', :limit => 5).size
|
||||
end
|
||||
|
||||
def test_order_on_join_table_with_include_and_limit
|
||||
assert_equal 5, Developer.find(:all, :include => 'projects', :order => 'developers_projects.joined_on DESC', :limit => 5).size
|
||||
end
|
||||
|
||||
def test_eager_loading_with_order_on_joined_table_preloads
|
||||
posts = assert_queries(2) do
|
||||
Post.find(:all, :joins => :comments, :include => :author, :order => 'comments.id DESC')
|
||||
end
|
||||
assert_equal posts(:eager_other), posts[0]
|
||||
assert_equal authors(:mary), assert_no_queries { posts[0].author}
|
||||
end
|
||||
|
||||
def test_eager_loading_with_conditions_on_joined_table_preloads
|
||||
posts = assert_queries(2) do
|
||||
Post.find(:all, :select => 'distinct posts.*', :include => :author, :joins => [:comments], :conditions => "comments.body like 'Thank you%'", :order => 'posts.id')
|
||||
end
|
||||
assert_equal [posts(:welcome)], posts
|
||||
assert_equal authors(:david), assert_no_queries { posts[0].author}
|
||||
|
||||
posts = assert_queries(2) do
|
||||
Post.find(:all, :select => 'distinct posts.*', :include => :author, :joins => [:comments], :conditions => "comments.body like 'Thank you%'", :order => 'posts.id')
|
||||
end
|
||||
assert_equal [posts(:welcome)], posts
|
||||
assert_equal authors(:david), assert_no_queries { posts[0].author}
|
||||
|
||||
posts = assert_queries(2) do
|
||||
Post.find(:all, :include => :author, :joins => {:taggings => :tag}, :conditions => "tags.name = 'General'", :order => 'posts.id')
|
||||
end
|
||||
assert_equal posts(:welcome, :thinking), posts
|
||||
|
||||
posts = assert_queries(2) do
|
||||
Post.find(:all, :include => :author, :joins => {:taggings => {:tag => :taggings}}, :conditions => "taggings_tags.super_tag_id=2", :order => 'posts.id')
|
||||
end
|
||||
assert_equal posts(:welcome, :thinking), posts
|
||||
|
||||
end
|
||||
|
||||
def test_eager_loading_with_conditions_on_string_joined_table_preloads
|
||||
posts = assert_queries(2) do
|
||||
Post.find(:all, :select => 'distinct posts.*', :include => :author, :joins => "INNER JOIN comments on comments.post_id = posts.id", :conditions => "comments.body like 'Thank you%'", :order => 'posts.id')
|
||||
end
|
||||
assert_equal [posts(:welcome)], posts
|
||||
assert_equal authors(:david), assert_no_queries { posts[0].author}
|
||||
|
||||
posts = assert_queries(2) do
|
||||
Post.find(:all, :select => 'distinct posts.*', :include => :author, :joins => ["INNER JOIN comments on comments.post_id = posts.id"], :conditions => "comments.body like 'Thank you%'", :order => 'posts.id')
|
||||
end
|
||||
assert_equal [posts(:welcome)], posts
|
||||
assert_equal authors(:david), assert_no_queries { posts[0].author}
|
||||
|
||||
end
|
||||
|
||||
def test_eager_loading_with_select_on_joined_table_preloads
|
||||
posts = assert_queries(2) do
|
||||
Post.find(:all, :select => 'posts.*, authors.name as author_name', :include => :comments, :joins => :author, :order => 'posts.id')
|
||||
end
|
||||
assert_equal 'David', posts[0].author_name
|
||||
assert_equal posts(:welcome).comments, assert_no_queries { posts[0].comments}
|
||||
end
|
||||
|
||||
def test_eager_loading_with_conditions_on_join_model_preloads
|
||||
authors = assert_queries(2) do
|
||||
Author.find(:all, :include => :author_address, :joins => :comments, :conditions => "posts.title like 'Welcome%'")
|
||||
end
|
||||
assert_equal authors(:david), authors[0]
|
||||
assert_equal author_addresses(:david_address), authors[0].author_address
|
||||
end
|
||||
|
||||
def test_preload_belongs_to_uses_exclusive_scope
|
||||
people = Person.males.find(:all, :include => :primary_contact)
|
||||
assert_not_equal people.length, 0
|
||||
people.each do |person|
|
||||
assert_no_queries {assert_not_nil person.primary_contact}
|
||||
assert_equal Person.find(person.id).primary_contact, person.primary_contact
|
||||
end
|
||||
end
|
||||
|
||||
def test_preload_has_many_uses_exclusive_scope
|
||||
people = Person.males.find :all, :include => :agents
|
||||
people.each do |person|
|
||||
assert_equal Person.find(person.id).agents, person.agents
|
||||
end
|
||||
end
|
||||
|
||||
def test_preload_has_many_using_primary_key
|
||||
expected = Firm.find(:first).clients_using_primary_key.to_a
|
||||
firm = Firm.find :first, :include => :clients_using_primary_key
|
||||
assert_no_queries do
|
||||
assert_equal expected, firm.clients_using_primary_key
|
||||
end
|
||||
end
|
||||
|
||||
def test_include_has_many_using_primary_key
|
||||
expected = Firm.find(1).clients_using_primary_key.sort_by &:name
|
||||
firm = Firm.find 1, :include => :clients_using_primary_key, :order => 'clients_using_primary_keys_companies.name'
|
||||
assert_no_queries do
|
||||
assert_equal expected, firm.clients_using_primary_key
|
||||
end
|
||||
end
|
||||
|
||||
def test_preload_has_one_using_primary_key
|
||||
expected = Firm.find(:first).account_using_primary_key
|
||||
firm = Firm.find :first, :include => :account_using_primary_key
|
||||
assert_no_queries do
|
||||
assert_equal expected, firm.account_using_primary_key
|
||||
end
|
||||
end
|
||||
|
||||
def test_include_has_one_using_primary_key
|
||||
expected = Firm.find(1).account_using_primary_key
|
||||
firm = Firm.find(:all, :include => :account_using_primary_key, :order => 'accounts.id').detect {|f| f.id == 1}
|
||||
assert_no_queries do
|
||||
assert_equal expected, firm.account_using_primary_key
|
||||
end
|
||||
end
|
||||
|
||||
def test_preloading_empty_polymorphic_parent
|
||||
t = Tagging.create!(:taggable_type => 'Post', :taggable_id => Post.maximum(:id) + 1, :tag => tags(:general))
|
||||
|
||||
assert_queries(2) { @tagging = Tagging.find(t.id, :include => :taggable) }
|
||||
assert_no_queries { assert ! @tagging.taggable }
|
||||
end
|
||||
end
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
require "cases/helper"
|
||||
require 'models/post'
|
||||
require 'models/comment'
|
||||
require 'models/project'
|
||||
require 'models/developer'
|
||||
require 'models/company_in_module'
|
||||
|
||||
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
|
||||
|
||||
|
||||
def test_extension_name
|
||||
extension = Proc.new {}
|
||||
name = :association_name
|
||||
|
||||
assert_equal 'DeveloperAssociationNameAssociationExtension', Developer.send(:create_extension_modules, name, extension, []).first.name
|
||||
assert_equal 'MyApplication::Business::DeveloperAssociationNameAssociationExtension',
|
||||
MyApplication::Business::Developer.send(:create_extension_modules, name, extension, []).first.name
|
||||
assert_equal 'MyApplication::Business::DeveloperAssociationNameAssociationExtension', MyApplication::Business::Developer.send(:create_extension_modules, name, extension, []).first.name
|
||||
assert_equal 'MyApplication::Business::DeveloperAssociationNameAssociationExtension', MyApplication::Business::Developer.send(:create_extension_modules, name, extension, []).first.name
|
||||
end
|
||||
|
||||
|
||||
end
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
require 'cases/helper'
|
||||
|
||||
class MyReader < ActiveRecord::Base
|
||||
has_and_belongs_to_many :my_books
|
||||
end
|
||||
|
||||
class MyBook < ActiveRecord::Base
|
||||
has_and_belongs_to_many :my_readers
|
||||
end
|
||||
|
||||
class HabtmJoinTableTest < ActiveRecord::TestCase
|
||||
def setup
|
||||
ActiveRecord::Base.connection.create_table :my_books, :force => true do |t|
|
||||
t.string :name
|
||||
end
|
||||
assert ActiveRecord::Base.connection.table_exists?(:my_books)
|
||||
|
||||
ActiveRecord::Base.connection.create_table :my_readers, :force => true do |t|
|
||||
t.string :name
|
||||
end
|
||||
assert ActiveRecord::Base.connection.table_exists?(:my_readers)
|
||||
|
||||
ActiveRecord::Base.connection.create_table :my_books_my_readers, :force => true do |t|
|
||||
t.integer :my_book_id
|
||||
t.integer :my_reader_id
|
||||
end
|
||||
assert ActiveRecord::Base.connection.table_exists?(:my_books_my_readers)
|
||||
end
|
||||
|
||||
def teardown
|
||||
ActiveRecord::Base.connection.drop_table :my_books
|
||||
ActiveRecord::Base.connection.drop_table :my_readers
|
||||
ActiveRecord::Base.connection.drop_table :my_books_my_readers
|
||||
end
|
||||
|
||||
uses_transaction :test_should_raise_exception_when_join_table_has_a_primary_key
|
||||
def test_should_raise_exception_when_join_table_has_a_primary_key
|
||||
if ActiveRecord::Base.connection.supports_primary_key?
|
||||
assert_raise ActiveRecord::ConfigurationError do
|
||||
jaime = MyReader.create(:name=>"Jaime")
|
||||
jaime.my_books << MyBook.create(:name=>'Great Expectations')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
uses_transaction :test_should_cache_result_of_primary_key_check
|
||||
def test_should_cache_result_of_primary_key_check
|
||||
if ActiveRecord::Base.connection.supports_primary_key?
|
||||
ActiveRecord::Base.connection.stubs(:primary_key).with('my_books_my_readers').returns(false).once
|
||||
weaz = MyReader.create(:name=>'Weaz')
|
||||
|
||||
weaz.my_books << MyBook.create(:name=>'Great Expectations')
|
||||
weaz.my_books << MyBook.create(:name=>'Greater Expectations')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,822 +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 DeveloperWithCounterSQL < ActiveRecord::Base
|
||||
set_table_name 'developers'
|
||||
has_and_belongs_to_many :projects,
|
||||
:class_name => "DeveloperWithCounterSQL",
|
||||
:join_table => "developers_projects",
|
||||
:association_foreign_key => "project_id",
|
||||
:foreign_key => "developer_id",
|
||||
:counter_sql => 'SELECT COUNT(*) AS count_all FROM projects INNER JOIN developers_projects ON projects.id = developers_projects.project_id WHERE developers_projects.developer_id =#{id}'
|
||||
end
|
||||
|
||||
class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
|
||||
fixtures :accounts, :companies, :categories, :posts, :categories_posts, :developers, :projects, :developers_projects,
|
||||
:parrots, :pirates, :treasures, :price_estimates, :tags, :taggings
|
||||
|
||||
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_uniq_option_prevents_duplicate_push
|
||||
project = projects(:active_record)
|
||||
project.developers << developers(:jamis)
|
||||
project.developers << developers(:david)
|
||||
assert_equal 3, project.developers.size
|
||||
|
||||
project.developers << developers(:david)
|
||||
project.developers << developers(:jamis)
|
||||
assert_equal 3, project.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_destroying
|
||||
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
|
||||
|
||||
assert_difference "Project.count", -1 do
|
||||
david.projects.destroy(active_record)
|
||||
end
|
||||
|
||||
assert_equal 1, david.reload.projects.size
|
||||
assert_equal 1, david.projects(true).size
|
||||
end
|
||||
|
||||
def test_destroying_array
|
||||
david = Developer.find(1)
|
||||
david.projects.reload
|
||||
|
||||
assert_difference "Project.count", -Project.count do
|
||||
david.projects.destroy(Project.find(:all))
|
||||
end
|
||||
|
||||
assert_equal 0, david.reload.projects.size
|
||||
assert_equal 0, david.projects(true).size
|
||||
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_multiple_interpolations
|
||||
# interpolate once:
|
||||
assert_equal [developers(:david), developers(:jamis), developers(:poor_jamis)], projects(:active_record).developers_with_finder_sql, "first interpolation"
|
||||
# interpolate again, for a different project id
|
||||
assert_equal [developers(:david)], projects(:action_controller).developers_with_finder_sql, "second interpolation"
|
||||
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_raise(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_find_grouped
|
||||
all_posts_from_category1 = Post.find(:all, :conditions => "category_id = 1", :joins => :categories)
|
||||
grouped_posts_of_category1 = Post.find(:all, :conditions => "category_id = 1", :group => "author_id", :select => 'count(posts.id) as posts_count', :joins => :categories)
|
||||
assert_equal 4, all_posts_from_category1.size
|
||||
assert_equal 1, grouped_posts_of_category1.size
|
||||
end
|
||||
|
||||
def test_find_scoped_grouped
|
||||
assert_equal 4, categories(:general).posts_gruoped_by_title.size
|
||||
assert_equal 1, categories(:technology).posts_gruoped_by_title.size
|
||||
end
|
||||
|
||||
def test_find_scoped_grouped_having
|
||||
assert_equal 2, projects(:active_record).well_payed_salary_groups.size
|
||||
assert projects(:active_record).well_payed_salary_groups.all? { |g| g.salary > 10000 }
|
||||
end
|
||||
|
||||
def test_get_ids
|
||||
assert_equal projects(:active_record, :action_controller).map(&:id).sort, developers(:david).project_ids.sort
|
||||
assert_equal [projects(:active_record).id], developers(:jamis).project_ids
|
||||
end
|
||||
|
||||
def test_get_ids_for_loaded_associations
|
||||
developer = developers(:david)
|
||||
developer.projects(true)
|
||||
assert_queries(0) do
|
||||
developer.project_ids
|
||||
developer.project_ids
|
||||
end
|
||||
end
|
||||
|
||||
def test_get_ids_for_unloaded_associations_does_not_load_them
|
||||
developer = developers(:david)
|
||||
assert !developer.projects.loaded?
|
||||
assert_equal projects(:active_record, :action_controller).map(&:id).sort, developer.project_ids.sort
|
||||
assert !developer.projects.loaded?
|
||||
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), projects(:action_controller)].map(&:id).sort, developer.project_ids.sort
|
||||
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), projects(:action_controller)].map(&:id).sort, developer.project_ids.sort
|
||||
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
|
||||
|
||||
def test_self_referential_habtm_without_foreign_key_set_should_raise_exception
|
||||
assert_raise(ActiveRecord::HasAndBelongsToManyAssociationForeignKeyNeeded) {
|
||||
Member.class_eval do
|
||||
has_and_belongs_to_many :friends, :class_name => "Member", :join_table => "member_friends"
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
def test_dynamic_find_should_respect_association_include
|
||||
# SQL error in sort clause if :include is not included
|
||||
# due to Unknown column 'authors.id'
|
||||
assert Category.find(1).posts_with_authors_sorted_by_author_id.find_by_title('Welcome to the weblog')
|
||||
end
|
||||
|
||||
def test_counting_on_habtm_association_and_not_array
|
||||
david = Developer.find(1)
|
||||
# Extra parameter just to make sure we aren't falling back to
|
||||
# Array#count in Ruby >=1.8.7, which would raise an ArgumentError
|
||||
assert_nothing_raised { david.projects.count(:all, :conditions => '1=1') }
|
||||
end
|
||||
|
||||
def test_count
|
||||
david = Developer.find(1)
|
||||
assert_equal 2, david.projects.count
|
||||
end
|
||||
|
||||
def test_count_with_counter_sql
|
||||
developer = DeveloperWithCounterSQL.create(:name => 'tekin')
|
||||
developer.project_ids = [projects(:active_record).id]
|
||||
developer.save
|
||||
developer.reload
|
||||
assert_equal 1, developer.projects.count
|
||||
end
|
||||
|
||||
def test_association_proxy_transaction_method_starts_transaction_in_association_class
|
||||
Post.expects(:transaction)
|
||||
Category.find(:first).posts.transaction do
|
||||
# nothing
|
||||
end
|
||||
end
|
||||
|
||||
def test_caching_of_columns
|
||||
david = Developer.find(1)
|
||||
# clear cache possibly created by other tests
|
||||
david.projects.reset_column_information
|
||||
assert_queries(0) { david.projects.columns; david.projects.columns }
|
||||
# and again to verify that reset_column_information clears the cache correctly
|
||||
david.projects.reset_column_information
|
||||
assert_queries(0) { david.projects.columns; david.projects.columns }
|
||||
end
|
||||
|
||||
end
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,346 +0,0 @@
|
|||
require "cases/helper"
|
||||
require 'models/post'
|
||||
require 'models/person'
|
||||
require 'models/reference'
|
||||
require 'models/job'
|
||||
require 'models/reader'
|
||||
require 'models/comment'
|
||||
require 'models/tag'
|
||||
require 'models/tagging'
|
||||
require 'models/author'
|
||||
require 'models/owner'
|
||||
require 'models/pet'
|
||||
require 'models/toy'
|
||||
require 'models/contract'
|
||||
require 'models/company'
|
||||
require 'models/developer'
|
||||
|
||||
class HasManyThroughAssociationsTest < ActiveRecord::TestCase
|
||||
fixtures :posts, :readers, :people, :comments, :authors, :owners, :pets, :toys,
|
||||
:companies
|
||||
|
||||
def test_associate_existing
|
||||
assert_queries(2) { posts(:thinking);people(:david) }
|
||||
|
||||
posts(:thinking).people
|
||||
|
||||
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_destroy_association
|
||||
assert_difference "Person.count", -1 do
|
||||
posts(:welcome).people.destroy(people(:michael))
|
||||
end
|
||||
|
||||
assert posts(:welcome).reload.people.empty?
|
||||
assert posts(:welcome).people(true).empty?
|
||||
end
|
||||
|
||||
def test_destroy_all
|
||||
assert_difference "Person.count", -1 do
|
||||
posts(:welcome).people.destroy_all
|
||||
end
|
||||
|
||||
assert posts(:welcome).reload.people.empty?
|
||||
assert posts(:welcome).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_replace_order_is_preserved
|
||||
posts(:welcome).people.clear
|
||||
posts(:welcome).people = [people(:david), people(:michael)]
|
||||
assert_equal [people(:david).id, people(:michael).id], posts(:welcome).readers.all(:order => 'id').map(&:person_id)
|
||||
|
||||
# Test the inverse order in case the first success was a coincidence
|
||||
posts(:welcome).people.clear
|
||||
posts(:welcome).people = [people(:michael), people(:david)]
|
||||
assert_equal [people(:michael).id, people(:david).id], posts(:welcome).readers.all(:order => 'id').map(&:person_id)
|
||||
end
|
||||
|
||||
def test_replace_by_id_order_is_preserved
|
||||
posts(:welcome).people.clear
|
||||
posts(:welcome).person_ids = [people(:david).id, people(:michael).id]
|
||||
assert_equal [people(:david).id, people(:michael).id], posts(:welcome).readers.all(:order => 'id').map(&:person_id)
|
||||
|
||||
# Test the inverse order in case the first success was a coincidence
|
||||
posts(:welcome).people.clear
|
||||
posts(:welcome).person_ids = [people(:michael).id, people(:david).id]
|
||||
assert_equal [people(:michael).id, people(:david).id], posts(:welcome).readers.all(:order => 'id').map(&:person_id)
|
||||
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_associate_with_create_and_invalid_options
|
||||
peeps = companies(:first_firm).developers.count
|
||||
assert_nothing_raised { companies(:first_firm).developers.create(:name => '0') }
|
||||
assert_equal peeps, companies(:first_firm).developers.count
|
||||
end
|
||||
|
||||
def test_associate_with_create_and_valid_options
|
||||
peeps = companies(:first_firm).developers.count
|
||||
assert_nothing_raised { companies(:first_firm).developers.create(:name => 'developer') }
|
||||
assert_equal peeps + 1, companies(:first_firm).developers.count
|
||||
end
|
||||
|
||||
def test_associate_with_create_bang_and_invalid_options
|
||||
peeps = companies(:first_firm).developers.count
|
||||
assert_raises(ActiveRecord::RecordInvalid) { companies(:first_firm).developers.create!(:name => '0') }
|
||||
assert_equal peeps, companies(:first_firm).developers.count
|
||||
end
|
||||
|
||||
def test_associate_with_create_bang_and_valid_options
|
||||
peeps = companies(:first_firm).developers.count
|
||||
assert_nothing_raised { companies(:first_firm).developers.create!(:name => 'developer') }
|
||||
assert_equal peeps + 1, companies(:first_firm).developers.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
|
||||
|
||||
def test_dynamic_find_should_respect_association_include
|
||||
# SQL error in sort clause if :include is not included
|
||||
# due to Unknown column 'comments.id'
|
||||
assert Person.find(1).posts_with_comments_sorted_by_comment_id.find_by_title('Welcome to the weblog')
|
||||
end
|
||||
|
||||
def test_count_with_include_should_alias_join_table
|
||||
assert_equal 2, people(:michael).posts.count(:include => :readers)
|
||||
end
|
||||
|
||||
def test_inner_join_with_quoted_table_name
|
||||
assert_equal 2, people(:michael).jobs.size
|
||||
end
|
||||
|
||||
def test_get_ids
|
||||
assert_equal [posts(:welcome).id, posts(:authorless).id].sort, people(:michael).post_ids.sort
|
||||
end
|
||||
|
||||
def test_get_ids_for_loaded_associations
|
||||
person = people(:michael)
|
||||
person.posts(true)
|
||||
assert_queries(0) do
|
||||
person.post_ids
|
||||
person.post_ids
|
||||
end
|
||||
end
|
||||
|
||||
def test_get_ids_for_unloaded_associations_does_not_load_them
|
||||
person = people(:michael)
|
||||
assert !person.posts.loaded?
|
||||
assert_equal [posts(:welcome).id, posts(:authorless).id].sort, person.post_ids.sort
|
||||
assert !person.posts.loaded?
|
||||
end
|
||||
|
||||
def test_association_proxy_transaction_method_starts_transaction_in_association_class
|
||||
Tag.expects(:transaction)
|
||||
Post.find(:first).tags.transaction do
|
||||
# nothing
|
||||
end
|
||||
end
|
||||
|
||||
def test_has_many_association_through_a_belongs_to_association_where_the_association_doesnt_exist
|
||||
author = authors(:mary)
|
||||
post = Post.create!(:title => "TITLE", :body => "BODY")
|
||||
assert_equal [], post.author_favorites
|
||||
end
|
||||
|
||||
def test_has_many_association_through_a_belongs_to_association
|
||||
author = authors(:mary)
|
||||
post = Post.create!(:author => author, :title => "TITLE", :body => "BODY")
|
||||
author.author_favorites.create(:favorite_author_id => 1)
|
||||
author.author_favorites.create(:favorite_author_id => 2)
|
||||
author.author_favorites.create(:favorite_author_id => 3)
|
||||
assert_equal post.author.author_favorites, post.author_favorites
|
||||
end
|
||||
|
||||
def test_has_many_association_through_a_has_many_association_with_nonstandard_primary_keys
|
||||
assert_equal 1, owners(:blackbeard).toys.count
|
||||
end
|
||||
|
||||
def test_find_on_has_many_association_collection_with_include_and_conditions
|
||||
post_with_no_comments = people(:michael).posts_with_no_comments.first
|
||||
assert_equal post_with_no_comments, posts(:authorless)
|
||||
end
|
||||
|
||||
def test_has_many_through_has_one_reflection
|
||||
assert_equal [comments(:eager_sti_on_associations_vs_comment)], authors(:david).very_special_comments
|
||||
end
|
||||
|
||||
def test_modifying_has_many_through_has_one_reflection_should_raise
|
||||
[
|
||||
lambda { authors(:david).very_special_comments = [VerySpecialComment.create!(:body => "Gorp!", :post_id => 1011), VerySpecialComment.create!(:body => "Eep!", :post_id => 1012)] },
|
||||
lambda { authors(:david).very_special_comments << VerySpecialComment.create!(:body => "Hoohah!", :post_id => 1013) },
|
||||
lambda { authors(:david).very_special_comments.delete(authors(:david).very_special_comments.first) },
|
||||
].each {|block| assert_raise(ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection, &block) }
|
||||
end
|
||||
end
|
||||
|
|
@ -1,330 +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_finding_using_primary_key
|
||||
firm = companies(:first_firm)
|
||||
assert_equal Account.find_by_firm_id(firm.id), firm.account
|
||||
firm.firm_id = companies(:rails_core).id
|
||||
assert_equal accounts(:rails_core_account), firm.account_using_primary_key
|
||||
end
|
||||
|
||||
def test_update_with_foreign_and_primary_keys
|
||||
firm = companies(:first_firm)
|
||||
account = firm.account_using_foreign_and_primary_keys
|
||||
assert_equal Account.find_by_firm_name(firm.name), account
|
||||
firm.save
|
||||
firm.reload
|
||||
assert_equal account, firm.account_using_foreign_and_primary_keys
|
||||
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_raise(ActiveRecord::AssociationTypeMismatch) { companies(:first_firm).account = 1 }
|
||||
assert_raise(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_raise(ActiveRecord::RecordNotFound) { Account.find(old_account_id) }
|
||||
end
|
||||
|
||||
def test_nullification_on_association_change
|
||||
firm = companies(:rails_core)
|
||||
old_account_id = firm.account.id
|
||||
firm.account = Account.new
|
||||
# account is dependent with nullify, therefore its firm_id should be nil
|
||||
assert_nil Account.find(old_account_id).firm_id
|
||||
end
|
||||
|
||||
def test_association_changecalls_delete
|
||||
companies(:first_firm).deletable_account = Account.new
|
||||
assert_equal [], Account.destroyed_account_ids[companies(:first_firm).id]
|
||||
end
|
||||
|
||||
def test_association_change_calls_destroy
|
||||
companies(:first_firm).account = Account.new
|
||||
assert_equal [companies(:first_firm).id], Account.destroyed_account_ids[companies(:first_firm).id]
|
||||
end
|
||||
|
||||
def test_natural_assignment_to_already_associated_record
|
||||
company = companies(:first_firm)
|
||||
account = accounts(:signals37)
|
||||
assert_equal company.account, account
|
||||
company.account = account
|
||||
company.reload
|
||||
account.reload
|
||||
assert_equal company.account, account
|
||||
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_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_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_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
|
||||
|
||||
def test_has_one_proxy_should_not_respond_to_private_methods
|
||||
assert_raise(NoMethodError) { accounts(:signals37).private_method }
|
||||
assert_raise(NoMethodError) { companies(:first_firm).account.private_method }
|
||||
end
|
||||
|
||||
def test_has_one_proxy_should_respond_to_private_methods_via_send
|
||||
accounts(:signals37).send(:private_method)
|
||||
companies(:first_firm).account.send(:private_method)
|
||||
end
|
||||
|
||||
def test_save_of_record_with_loaded_has_one
|
||||
@firm = companies(:first_firm)
|
||||
assert_not_nil @firm.account
|
||||
|
||||
assert_nothing_raised do
|
||||
Firm.find(@firm.id).save!
|
||||
Firm.find(@firm.id, :include => :account).save!
|
||||
end
|
||||
|
||||
@firm.account.destroy
|
||||
|
||||
assert_nothing_raised do
|
||||
Firm.find(@firm.id).save!
|
||||
Firm.find(@firm.id, :include => :account).save!
|
||||
end
|
||||
end
|
||||
|
||||
def test_build_respects_hash_condition
|
||||
account = companies(:first_firm).build_account_limit_500_with_hash_conditions
|
||||
assert account.save
|
||||
assert_equal 500, account.credit_limit
|
||||
end
|
||||
|
||||
def test_create_respects_hash_condition
|
||||
account = companies(:first_firm).create_account_limit_500_with_hash_conditions
|
||||
assert !account.new_record?
|
||||
assert_equal 500, account.credit_limit
|
||||
end
|
||||
end
|
||||
|
|
@ -1,209 +0,0 @@
|
|||
require "cases/helper"
|
||||
require 'models/club'
|
||||
require 'models/member_type'
|
||||
require 'models/member'
|
||||
require 'models/membership'
|
||||
require 'models/sponsor'
|
||||
require 'models/organization'
|
||||
require 'models/member_detail'
|
||||
|
||||
class HasOneThroughAssociationsTest < ActiveRecord::TestCase
|
||||
fixtures :member_types, :members, :clubs, :memberships, :sponsors, :organizations
|
||||
|
||||
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_creating_association_builds_through_record_for_new
|
||||
new_member = Member.new(:name => "Jane")
|
||||
new_member.club = clubs(:moustache_club)
|
||||
assert new_member.current_membership
|
||||
assert_equal clubs(:moustache_club), new_member.current_membership.club
|
||||
assert_equal clubs(:moustache_club), new_member.club
|
||||
assert new_member.save
|
||||
assert_equal clubs(:moustache_club), 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_set_record_to_nil_should_delete_association
|
||||
@member.club = nil
|
||||
@member.reload
|
||||
assert_equal nil, @member.current_membership
|
||||
assert_nil @member.club
|
||||
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 = assert_queries(3) do #base table, through table, clubs table
|
||||
Member.find(:all, :include => :club, :conditions => ["name = ?", "Groucho Marx"])
|
||||
end
|
||||
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 = assert_queries(3) do #base table, through table, clubs table
|
||||
Member.find(:all, :include => :sponsor_club, :conditions => ["name = ?", "Groucho Marx"])
|
||||
end
|
||||
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
|
||||
|
||||
def test_has_one_through_nonpreload_eagerloading
|
||||
members = assert_queries(1) do
|
||||
Member.find(:all, :include => :club, :conditions => ["members.name = ?", "Groucho Marx"], :order => 'clubs.name') #force fallback
|
||||
end
|
||||
assert_equal 1, members.size
|
||||
assert_not_nil assert_no_queries {members[0].club}
|
||||
end
|
||||
|
||||
def test_has_one_through_nonpreload_eager_loading_through_polymorphic
|
||||
members = assert_queries(1) do
|
||||
Member.find(:all, :include => :sponsor_club, :conditions => ["members.name = ?", "Groucho Marx"], :order => 'clubs.name') #force fallback
|
||||
end
|
||||
assert_equal 1, members.size
|
||||
assert_not_nil assert_no_queries {members[0].sponsor_club}
|
||||
end
|
||||
|
||||
def test_has_one_through_nonpreload_eager_loading_through_polymorphic_with_more_than_one_through_record
|
||||
Sponsor.new(:sponsor_club => clubs(:crazy_club), :sponsorable => members(:groucho)).save!
|
||||
members = assert_queries(1) do
|
||||
Member.find(:all, :include => :sponsor_club, :conditions => ["members.name = ?", "Groucho Marx"], :order => 'clubs.name DESC') #force fallback
|
||||
end
|
||||
assert_equal 1, members.size
|
||||
assert_not_nil assert_no_queries { members[0].sponsor_club }
|
||||
assert_equal clubs(:crazy_club), members[0].sponsor_club
|
||||
end
|
||||
|
||||
def test_uninitialized_has_one_through_should_return_nil_for_unsaved_record
|
||||
assert_nil Member.new.club
|
||||
end
|
||||
|
||||
def test_assigning_association_correctly_assigns_target
|
||||
new_member = Member.create(:name => "Chris")
|
||||
new_member.club = new_club = Club.create(:name => "LRUG")
|
||||
assert_equal new_club, new_member.club.target
|
||||
end
|
||||
|
||||
def test_has_one_through_proxy_should_not_respond_to_private_methods
|
||||
assert_raise(NoMethodError) { clubs(:moustache_club).private_method }
|
||||
assert_raise(NoMethodError) { @member.club.private_method }
|
||||
end
|
||||
|
||||
def test_has_one_through_proxy_should_respond_to_private_methods_via_send
|
||||
clubs(:moustache_club).send(:private_method)
|
||||
@member.club.send(:private_method)
|
||||
end
|
||||
|
||||
def test_assigning_to_has_one_through_preserves_decorated_join_record
|
||||
@organization = organizations(:nsa)
|
||||
assert_difference 'MemberDetail.count', 1 do
|
||||
@member_detail = MemberDetail.new(:extra_data => 'Extra')
|
||||
@member.member_detail = @member_detail
|
||||
@member.organization = @organization
|
||||
end
|
||||
assert_equal @organization, @member.organization
|
||||
assert @organization.members.include?(@member)
|
||||
assert_equal 'Extra', @member.member_detail.extra_data
|
||||
end
|
||||
|
||||
def test_reassigning_has_one_through
|
||||
@organization = organizations(:nsa)
|
||||
@new_organization = organizations(:discordians)
|
||||
|
||||
assert_difference 'MemberDetail.count', 1 do
|
||||
@member_detail = MemberDetail.new(:extra_data => 'Extra')
|
||||
@member.member_detail = @member_detail
|
||||
@member.organization = @organization
|
||||
end
|
||||
assert_equal @organization, @member.organization
|
||||
assert_equal 'Extra', @member.member_detail.extra_data
|
||||
assert @organization.members.include?(@member)
|
||||
assert !@new_organization.members.include?(@member)
|
||||
|
||||
assert_no_difference 'MemberDetail.count' do
|
||||
@member.organization = @new_organization
|
||||
end
|
||||
assert_equal @new_organization, @member.organization
|
||||
assert_equal 'Extra', @member.member_detail.extra_data
|
||||
assert !@organization.members.include?(@member)
|
||||
assert @new_organization.members.include?(@member)
|
||||
end
|
||||
|
||||
def test_preloading_has_one_through_on_belongs_to
|
||||
assert_not_nil @member.member_type
|
||||
@organization = organizations(:nsa)
|
||||
@member_detail = MemberDetail.new
|
||||
@member.member_detail = @member_detail
|
||||
@member.organization = @organization
|
||||
@member_details = assert_queries(3) do
|
||||
MemberDetail.find(:all, :include => :member_type)
|
||||
end
|
||||
@new_detail = @member_details[0]
|
||||
assert @new_detail.loaded_member_type?
|
||||
assert_not_nil assert_no_queries { @new_detail.member_type }
|
||||
end
|
||||
|
||||
def test_save_of_record_with_loaded_has_one_through
|
||||
@club = @member.club
|
||||
assert_not_nil @club.sponsored_member
|
||||
|
||||
assert_nothing_raised do
|
||||
Club.find(@club.id).save!
|
||||
Club.find(@club.id, :include => :sponsored_member).save!
|
||||
end
|
||||
|
||||
@club.sponsor.destroy
|
||||
|
||||
assert_nothing_raised do
|
||||
Club.find(@club.id).save!
|
||||
Club.find(@club.id, :include => :sponsored_member).save!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,93 +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_applies_aliases_tables_on_association_conditions
|
||||
result = Author.find(:all, :joins => [:thinking_posts, :welcome_posts])
|
||||
assert_equal authors(:david), result.first
|
||||
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,566 +0,0 @@
|
|||
require "cases/helper"
|
||||
require 'models/man'
|
||||
require 'models/face'
|
||||
require 'models/interest'
|
||||
require 'models/zine'
|
||||
require 'models/club'
|
||||
require 'models/sponsor'
|
||||
|
||||
class InverseAssociationTests < ActiveRecord::TestCase
|
||||
def test_should_allow_for_inverse_of_options_in_associations
|
||||
assert_nothing_raised(ArgumentError, 'ActiveRecord should allow the inverse_of options on has_many') do
|
||||
Class.new(ActiveRecord::Base).has_many(:wheels, :inverse_of => :car)
|
||||
end
|
||||
|
||||
assert_nothing_raised(ArgumentError, 'ActiveRecord should allow the inverse_of options on has_one') do
|
||||
Class.new(ActiveRecord::Base).has_one(:engine, :inverse_of => :car)
|
||||
end
|
||||
|
||||
assert_nothing_raised(ArgumentError, 'ActiveRecord should allow the inverse_of options on belongs_to') do
|
||||
Class.new(ActiveRecord::Base).belongs_to(:car, :inverse_of => :driver)
|
||||
end
|
||||
end
|
||||
|
||||
def test_should_be_able_to_ask_a_reflection_if_it_has_an_inverse
|
||||
has_one_with_inverse_ref = Man.reflect_on_association(:face)
|
||||
assert has_one_with_inverse_ref.respond_to?(:has_inverse?)
|
||||
assert has_one_with_inverse_ref.has_inverse?
|
||||
|
||||
has_many_with_inverse_ref = Man.reflect_on_association(:interests)
|
||||
assert has_many_with_inverse_ref.respond_to?(:has_inverse?)
|
||||
assert has_many_with_inverse_ref.has_inverse?
|
||||
|
||||
belongs_to_with_inverse_ref = Face.reflect_on_association(:man)
|
||||
assert belongs_to_with_inverse_ref.respond_to?(:has_inverse?)
|
||||
assert belongs_to_with_inverse_ref.has_inverse?
|
||||
|
||||
has_one_without_inverse_ref = Club.reflect_on_association(:sponsor)
|
||||
assert has_one_without_inverse_ref.respond_to?(:has_inverse?)
|
||||
assert !has_one_without_inverse_ref.has_inverse?
|
||||
|
||||
has_many_without_inverse_ref = Club.reflect_on_association(:memberships)
|
||||
assert has_many_without_inverse_ref.respond_to?(:has_inverse?)
|
||||
assert !has_many_without_inverse_ref.has_inverse?
|
||||
|
||||
belongs_to_without_inverse_ref = Sponsor.reflect_on_association(:sponsor_club)
|
||||
assert belongs_to_without_inverse_ref.respond_to?(:has_inverse?)
|
||||
assert !belongs_to_without_inverse_ref.has_inverse?
|
||||
end
|
||||
|
||||
def test_should_be_able_to_ask_a_reflection_what_it_is_the_inverse_of
|
||||
has_one_ref = Man.reflect_on_association(:face)
|
||||
assert has_one_ref.respond_to?(:inverse_of)
|
||||
|
||||
has_many_ref = Man.reflect_on_association(:interests)
|
||||
assert has_many_ref.respond_to?(:inverse_of)
|
||||
|
||||
belongs_to_ref = Face.reflect_on_association(:man)
|
||||
assert belongs_to_ref.respond_to?(:inverse_of)
|
||||
end
|
||||
|
||||
def test_inverse_of_method_should_supply_the_actual_reflection_instance_it_is_the_inverse_of
|
||||
has_one_ref = Man.reflect_on_association(:face)
|
||||
assert_equal Face.reflect_on_association(:man), has_one_ref.inverse_of
|
||||
|
||||
has_many_ref = Man.reflect_on_association(:interests)
|
||||
assert_equal Interest.reflect_on_association(:man), has_many_ref.inverse_of
|
||||
|
||||
belongs_to_ref = Face.reflect_on_association(:man)
|
||||
assert_equal Man.reflect_on_association(:face), belongs_to_ref.inverse_of
|
||||
end
|
||||
|
||||
def test_associations_with_no_inverse_of_should_return_nil
|
||||
has_one_ref = Club.reflect_on_association(:sponsor)
|
||||
assert_nil has_one_ref.inverse_of
|
||||
|
||||
has_many_ref = Club.reflect_on_association(:memberships)
|
||||
assert_nil has_many_ref.inverse_of
|
||||
|
||||
belongs_to_ref = Sponsor.reflect_on_association(:sponsor_club)
|
||||
assert_nil belongs_to_ref.inverse_of
|
||||
end
|
||||
end
|
||||
|
||||
class InverseHasOneTests < ActiveRecord::TestCase
|
||||
fixtures :men, :faces
|
||||
|
||||
def test_parent_instance_should_be_shared_with_child_on_find
|
||||
m = men(:gordon)
|
||||
f = m.face
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance"
|
||||
m.name = 'Bongo'
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same after changes to parent instance"
|
||||
f.man.name = 'Mungo'
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same after changes to child-owned instance"
|
||||
end
|
||||
|
||||
def test_parent_instance_should_be_shared_with_eager_loaded_child_on_find
|
||||
m = Man.find(:first, :conditions => {:name => 'Gordon'}, :include => :face)
|
||||
f = m.face
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance"
|
||||
m.name = 'Bongo'
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same after changes to parent instance"
|
||||
f.man.name = 'Mungo'
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same after changes to child-owned instance"
|
||||
|
||||
m = Man.find(:first, :conditions => {:name => 'Gordon'}, :include => :face, :order => 'faces.id')
|
||||
f = m.face
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance"
|
||||
m.name = 'Bongo'
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same after changes to parent instance"
|
||||
f.man.name = 'Mungo'
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same after changes to child-owned instance"
|
||||
end
|
||||
|
||||
def test_parent_instance_should_be_shared_with_newly_built_child
|
||||
m = men(:gordon)
|
||||
f = m.build_face(:description => 'haunted')
|
||||
assert_not_nil f.man
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance"
|
||||
m.name = 'Bongo'
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same after changes to parent instance"
|
||||
f.man.name = 'Mungo'
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same after changes to just-built-child-owned instance"
|
||||
end
|
||||
|
||||
def test_parent_instance_should_be_shared_with_newly_created_child
|
||||
m = men(:gordon)
|
||||
f = m.create_face(:description => 'haunted')
|
||||
assert_not_nil f.man
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance"
|
||||
m.name = 'Bongo'
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same after changes to parent instance"
|
||||
f.man.name = 'Mungo'
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same after changes to newly-created-child-owned instance"
|
||||
end
|
||||
|
||||
def test_parent_instance_should_be_shared_with_newly_created_child_via_bang_method
|
||||
m = Man.find(:first)
|
||||
f = m.face.create!(:description => 'haunted')
|
||||
assert_not_nil f.man
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance"
|
||||
m.name = 'Bongo'
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same after changes to parent instance"
|
||||
f.man.name = 'Mungo'
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same after changes to newly-created-child-owned instance"
|
||||
end
|
||||
|
||||
def test_parent_instance_should_be_shared_with_newly_built_child_when_we_dont_replace_existing
|
||||
m = Man.find(:first)
|
||||
f = m.build_face({:description => 'haunted'}, false)
|
||||
assert_not_nil f.man
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance"
|
||||
m.name = 'Bongo'
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same after changes to parent instance"
|
||||
f.man.name = 'Mungo'
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same after changes to just-built-child-owned instance"
|
||||
end
|
||||
|
||||
def test_parent_instance_should_be_shared_with_newly_created_child_when_we_dont_replace_existing
|
||||
m = Man.find(:first)
|
||||
f = m.create_face({:description => 'haunted'}, false)
|
||||
assert_not_nil f.man
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance"
|
||||
m.name = 'Bongo'
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same after changes to parent instance"
|
||||
f.man.name = 'Mungo'
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same after changes to newly-created-child-owned instance"
|
||||
end
|
||||
|
||||
def test_parent_instance_should_be_shared_with_newly_created_child_via_bang_method_when_we_dont_replace_existing
|
||||
m = Man.find(:first)
|
||||
f = m.face.create!({:description => 'haunted'}, false)
|
||||
assert_not_nil f.man
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance"
|
||||
m.name = 'Bongo'
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same after changes to parent instance"
|
||||
f.man.name = 'Mungo'
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same after changes to newly-created-child-owned instance"
|
||||
end
|
||||
|
||||
def test_parent_instance_should_be_shared_with_replaced_via_accessor_child
|
||||
m = Man.find(:first)
|
||||
f = Face.new(:description => 'haunted')
|
||||
m.face = f
|
||||
assert_not_nil f.man
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance"
|
||||
m.name = 'Bongo'
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same after changes to parent instance"
|
||||
f.man.name = 'Mungo'
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same after changes to replaced-child-owned instance"
|
||||
end
|
||||
|
||||
def test_parent_instance_should_be_shared_with_replaced_via_method_child
|
||||
m = Man.find(:first)
|
||||
f = Face.new(:description => 'haunted')
|
||||
m.face.replace(f)
|
||||
assert_not_nil f.man
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance"
|
||||
m.name = 'Bongo'
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same after changes to parent instance"
|
||||
f.man.name = 'Mungo'
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same after changes to replaced-child-owned instance"
|
||||
end
|
||||
|
||||
def test_parent_instance_should_be_shared_with_replaced_via_method_child_when_we_dont_replace_existing
|
||||
m = Man.find(:first)
|
||||
f = Face.new(:description => 'haunted')
|
||||
m.face.replace(f, false)
|
||||
assert_not_nil f.man
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance"
|
||||
m.name = 'Bongo'
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same after changes to parent instance"
|
||||
f.man.name = 'Mungo'
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same after changes to replaced-child-owned instance"
|
||||
end
|
||||
|
||||
def test_trying_to_use_inverses_that_dont_exist_should_raise_an_error
|
||||
assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Man.find(:first).dirty_face }
|
||||
end
|
||||
end
|
||||
|
||||
class InverseHasManyTests < ActiveRecord::TestCase
|
||||
fixtures :men, :interests
|
||||
|
||||
def test_parent_instance_should_be_shared_with_every_child_on_find
|
||||
m = men(:gordon)
|
||||
is = m.interests
|
||||
is.each do |i|
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance"
|
||||
m.name = 'Bongo'
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same after changes to parent instance"
|
||||
i.man.name = 'Mungo'
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same after changes to child-owned instance"
|
||||
end
|
||||
end
|
||||
|
||||
def test_parent_instance_should_be_shared_with_eager_loaded_children
|
||||
m = Man.find(:first, :conditions => {:name => 'Gordon'}, :include => :interests)
|
||||
is = m.interests
|
||||
is.each do |i|
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance"
|
||||
m.name = 'Bongo'
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same after changes to parent instance"
|
||||
i.man.name = 'Mungo'
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same after changes to child-owned instance"
|
||||
end
|
||||
|
||||
m = Man.find(:first, :conditions => {:name => 'Gordon'}, :include => :interests, :order => 'interests.id')
|
||||
is = m.interests
|
||||
is.each do |i|
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance"
|
||||
m.name = 'Bongo'
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same after changes to parent instance"
|
||||
i.man.name = 'Mungo'
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same after changes to child-owned instance"
|
||||
end
|
||||
end
|
||||
|
||||
def test_parent_instance_should_be_shared_with_newly_built_child
|
||||
m = men(:gordon)
|
||||
i = m.interests.build(:topic => 'Industrial Revolution Re-enactment')
|
||||
assert_not_nil i.man
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance"
|
||||
m.name = 'Bongo'
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same after changes to parent instance"
|
||||
i.man.name = 'Mungo'
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same after changes to just-built-child-owned instance"
|
||||
end
|
||||
|
||||
def test_parent_instance_should_be_shared_with_newly_block_style_built_child
|
||||
m = Man.find(:first)
|
||||
i = m.interests.build {|ii| ii.topic = 'Industrial Revolution Re-enactment'}
|
||||
assert_not_nil i.topic, "Child attributes supplied to build via blocks should be populated"
|
||||
assert_not_nil i.man
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance"
|
||||
m.name = 'Bongo'
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same after changes to parent instance"
|
||||
i.man.name = 'Mungo'
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same after changes to just-built-child-owned instance"
|
||||
end
|
||||
|
||||
def test_parent_instance_should_be_shared_with_newly_created_child
|
||||
m = men(:gordon)
|
||||
i = m.interests.create(:topic => 'Industrial Revolution Re-enactment')
|
||||
assert_not_nil i.man
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance"
|
||||
m.name = 'Bongo'
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same after changes to parent instance"
|
||||
i.man.name = 'Mungo'
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same after changes to newly-created-child-owned instance"
|
||||
end
|
||||
|
||||
def test_parent_instance_should_be_shared_with_newly_created_via_bang_method_child
|
||||
m = Man.find(:first)
|
||||
i = m.interests.create!(:topic => 'Industrial Revolution Re-enactment')
|
||||
assert_not_nil i.man
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance"
|
||||
m.name = 'Bongo'
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same after changes to parent instance"
|
||||
i.man.name = 'Mungo'
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same after changes to newly-created-child-owned instance"
|
||||
end
|
||||
|
||||
def test_parent_instance_should_be_shared_with_newly_block_style_created_child
|
||||
m = Man.find(:first)
|
||||
i = m.interests.create {|ii| ii.topic = 'Industrial Revolution Re-enactment'}
|
||||
assert_not_nil i.topic, "Child attributes supplied to create via blocks should be populated"
|
||||
assert_not_nil i.man
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance"
|
||||
m.name = 'Bongo'
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same after changes to parent instance"
|
||||
i.man.name = 'Mungo'
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same after changes to newly-created-child-owned instance"
|
||||
end
|
||||
|
||||
def test_parent_instance_should_be_shared_with_poked_in_child
|
||||
m = men(:gordon)
|
||||
i = Interest.create(:topic => 'Industrial Revolution Re-enactment')
|
||||
m.interests << i
|
||||
assert_not_nil i.man
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance"
|
||||
m.name = 'Bongo'
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same after changes to parent instance"
|
||||
i.man.name = 'Mungo'
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same after changes to newly-created-child-owned instance"
|
||||
end
|
||||
|
||||
def test_parent_instance_should_be_shared_with_replaced_via_accessor_children
|
||||
m = Man.find(:first)
|
||||
i = Interest.new(:topic => 'Industrial Revolution Re-enactment')
|
||||
m.interests = [i]
|
||||
assert_not_nil i.man
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance"
|
||||
m.name = 'Bongo'
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same after changes to parent instance"
|
||||
i.man.name = 'Mungo'
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same after changes to replaced-child-owned instance"
|
||||
end
|
||||
|
||||
def test_parent_instance_should_be_shared_with_replaced_via_method_children
|
||||
m = Man.find(:first)
|
||||
i = Interest.new(:topic => 'Industrial Revolution Re-enactment')
|
||||
m.interests.replace([i])
|
||||
assert_not_nil i.man
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance"
|
||||
m.name = 'Bongo'
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same after changes to parent instance"
|
||||
i.man.name = 'Mungo'
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same after changes to replaced-child-owned instance"
|
||||
end
|
||||
|
||||
def test_trying_to_use_inverses_that_dont_exist_should_raise_an_error
|
||||
assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Man.find(:first).secret_interests }
|
||||
end
|
||||
end
|
||||
|
||||
class InverseBelongsToTests < ActiveRecord::TestCase
|
||||
fixtures :men, :faces, :interests
|
||||
|
||||
def test_child_instance_should_be_shared_with_parent_on_find
|
||||
f = faces(:trusting)
|
||||
m = f.man
|
||||
assert_equal f.description, m.face.description, "Description of face should be the same before changes to child instance"
|
||||
f.description = 'gormless'
|
||||
assert_equal f.description, m.face.description, "Description of face should be the same after changes to child instance"
|
||||
m.face.description = 'pleasing'
|
||||
assert_equal f.description, m.face.description, "Description of face should be the same after changes to parent-owned instance"
|
||||
end
|
||||
|
||||
def test_eager_loaded_child_instance_should_be_shared_with_parent_on_find
|
||||
f = Face.find(:first, :include => :man, :conditions => {:description => 'trusting'})
|
||||
m = f.man
|
||||
assert_equal f.description, m.face.description, "Description of face should be the same before changes to child instance"
|
||||
f.description = 'gormless'
|
||||
assert_equal f.description, m.face.description, "Description of face should be the same after changes to child instance"
|
||||
m.face.description = 'pleasing'
|
||||
assert_equal f.description, m.face.description, "Description of face should be the same after changes to parent-owned instance"
|
||||
|
||||
f = Face.find(:first, :include => :man, :order => 'men.id', :conditions => {:description => 'trusting'})
|
||||
m = f.man
|
||||
assert_equal f.description, m.face.description, "Description of face should be the same before changes to child instance"
|
||||
f.description = 'gormless'
|
||||
assert_equal f.description, m.face.description, "Description of face should be the same after changes to child instance"
|
||||
m.face.description = 'pleasing'
|
||||
assert_equal f.description, m.face.description, "Description of face should be the same after changes to parent-owned instance"
|
||||
end
|
||||
|
||||
def test_child_instance_should_be_shared_with_newly_built_parent
|
||||
f = faces(:trusting)
|
||||
m = f.build_man(:name => 'Charles')
|
||||
assert_not_nil m.face
|
||||
assert_equal f.description, m.face.description, "Description of face should be the same before changes to child instance"
|
||||
f.description = 'gormless'
|
||||
assert_equal f.description, m.face.description, "Description of face should be the same after changes to child instance"
|
||||
m.face.description = 'pleasing'
|
||||
assert_equal f.description, m.face.description, "Description of face should be the same after changes to just-built-parent-owned instance"
|
||||
end
|
||||
|
||||
def test_child_instance_should_be_shared_with_newly_created_parent
|
||||
f = faces(:trusting)
|
||||
m = f.create_man(:name => 'Charles')
|
||||
assert_not_nil m.face
|
||||
assert_equal f.description, m.face.description, "Description of face should be the same before changes to child instance"
|
||||
f.description = 'gormless'
|
||||
assert_equal f.description, m.face.description, "Description of face should be the same after changes to child instance"
|
||||
m.face.description = 'pleasing'
|
||||
assert_equal f.description, m.face.description, "Description of face should be the same after changes to newly-created-parent-owned instance"
|
||||
end
|
||||
|
||||
def test_should_not_try_to_set_inverse_instances_when_the_inverse_is_a_has_many
|
||||
i = interests(:trainspotting)
|
||||
m = i.man
|
||||
assert_not_nil m.interests
|
||||
iz = m.interests.detect {|iz| iz.id == i.id}
|
||||
assert_not_nil iz
|
||||
assert_equal i.topic, iz.topic, "Interest topics should be the same before changes to child"
|
||||
i.topic = 'Eating cheese with a spoon'
|
||||
assert_not_equal i.topic, iz.topic, "Interest topics should not be the same after changes to child"
|
||||
iz.topic = 'Cow tipping'
|
||||
assert_not_equal i.topic, iz.topic, "Interest topics should not be the same after changes to parent-owned instance"
|
||||
end
|
||||
|
||||
def test_child_instance_should_be_shared_with_replaced_via_accessor_parent
|
||||
f = Face.find(:first)
|
||||
m = Man.new(:name => 'Charles')
|
||||
f.man = m
|
||||
assert_not_nil m.face
|
||||
assert_equal f.description, m.face.description, "Description of face should be the same before changes to child instance"
|
||||
f.description = 'gormless'
|
||||
assert_equal f.description, m.face.description, "Description of face should be the same after changes to child instance"
|
||||
m.face.description = 'pleasing'
|
||||
assert_equal f.description, m.face.description, "Description of face should be the same after changes to replaced-parent-owned instance"
|
||||
end
|
||||
|
||||
def test_child_instance_should_be_shared_with_replaced_via_method_parent
|
||||
f = faces(:trusting)
|
||||
assert_not_nil f.man
|
||||
m = Man.new(:name => 'Charles')
|
||||
f.man.replace(m)
|
||||
assert_not_nil m.face
|
||||
assert_equal f.description, m.face.description, "Description of face should be the same before changes to child instance"
|
||||
f.description = 'gormless'
|
||||
assert_equal f.description, m.face.description, "Description of face should be the same after changes to child instance"
|
||||
m.face.description = 'pleasing'
|
||||
assert_equal f.description, m.face.description, "Description of face should be the same after changes to replaced-parent-owned instance"
|
||||
end
|
||||
|
||||
def test_trying_to_use_inverses_that_dont_exist_should_raise_an_error
|
||||
assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Face.find(:first).horrible_man }
|
||||
end
|
||||
end
|
||||
|
||||
class InversePolymorphicBelongsToTests < ActiveRecord::TestCase
|
||||
fixtures :men, :faces, :interests
|
||||
|
||||
def test_child_instance_should_be_shared_with_parent_on_find
|
||||
f = Face.find(:first, :conditions => {:description => 'confused'})
|
||||
m = f.polymorphic_man
|
||||
assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same before changes to child instance"
|
||||
f.description = 'gormless'
|
||||
assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same after changes to child instance"
|
||||
m.polymorphic_face.description = 'pleasing'
|
||||
assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same after changes to parent-owned instance"
|
||||
end
|
||||
|
||||
def test_eager_loaded_child_instance_should_be_shared_with_parent_on_find
|
||||
f = Face.find(:first, :conditions => {:description => 'confused'}, :include => :man)
|
||||
m = f.polymorphic_man
|
||||
assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same before changes to child instance"
|
||||
f.description = 'gormless'
|
||||
assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same after changes to child instance"
|
||||
m.polymorphic_face.description = 'pleasing'
|
||||
assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same after changes to parent-owned instance"
|
||||
|
||||
f = Face.find(:first, :conditions => {:description => 'confused'}, :include => :man, :order => 'men.id')
|
||||
m = f.polymorphic_man
|
||||
assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same before changes to child instance"
|
||||
f.description = 'gormless'
|
||||
assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same after changes to child instance"
|
||||
m.polymorphic_face.description = 'pleasing'
|
||||
assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same after changes to parent-owned instance"
|
||||
end
|
||||
|
||||
def test_child_instance_should_be_shared_with_replaced_via_accessor_parent
|
||||
face = faces(:confused)
|
||||
old_man = face.polymorphic_man
|
||||
new_man = Man.new
|
||||
|
||||
assert_not_nil face.polymorphic_man
|
||||
face.polymorphic_man = new_man
|
||||
|
||||
assert_equal face.description, new_man.polymorphic_face.description, "Description of face should be the same before changes to parent instance"
|
||||
face.description = 'Bongo'
|
||||
assert_equal face.description, new_man.polymorphic_face.description, "Description of face should be the same after changes to parent instance"
|
||||
new_man.polymorphic_face.description = 'Mungo'
|
||||
assert_equal face.description, new_man.polymorphic_face.description, "Description of face should be the same after changes to replaced-parent-owned instance"
|
||||
end
|
||||
|
||||
def test_child_instance_should_be_shared_with_replaced_via_method_parent
|
||||
face = faces(:confused)
|
||||
old_man = face.polymorphic_man
|
||||
new_man = Man.new
|
||||
|
||||
assert_not_nil face.polymorphic_man
|
||||
face.polymorphic_man.replace(new_man)
|
||||
|
||||
assert_equal face.description, new_man.polymorphic_face.description, "Description of face should be the same before changes to parent instance"
|
||||
face.description = 'Bongo'
|
||||
assert_equal face.description, new_man.polymorphic_face.description, "Description of face should be the same after changes to parent instance"
|
||||
new_man.polymorphic_face.description = 'Mungo'
|
||||
assert_equal face.description, new_man.polymorphic_face.description, "Description of face should be the same after changes to replaced-parent-owned instance"
|
||||
end
|
||||
|
||||
def test_should_not_try_to_set_inverse_instances_when_the_inverse_is_a_has_many
|
||||
i = interests(:llama_wrangling)
|
||||
m = i.polymorphic_man
|
||||
assert_not_nil m.polymorphic_interests
|
||||
iz = m.polymorphic_interests.detect {|iz| iz.id == i.id}
|
||||
assert_not_nil iz
|
||||
assert_equal i.topic, iz.topic, "Interest topics should be the same before changes to child"
|
||||
i.topic = 'Eating cheese with a spoon'
|
||||
assert_not_equal i.topic, iz.topic, "Interest topics should not be the same after changes to child"
|
||||
iz.topic = 'Cow tipping'
|
||||
assert_not_equal i.topic, iz.topic, "Interest topics should not be the same after changes to parent-owned instance"
|
||||
end
|
||||
|
||||
def test_trying_to_access_inverses_that_dont_exist_shouldnt_raise_an_error
|
||||
# Ideally this would, if only for symmetry's sake with other association types
|
||||
assert_nothing_raised(ActiveRecord::InverseOfAssociationNotFoundError) { Face.find(:first).horrible_polymorphic_man }
|
||||
end
|
||||
|
||||
def test_trying_to_set_polymorphic_inverses_that_dont_exist_at_all_should_raise_an_error
|
||||
# fails because no class has the correct inverse_of for horrible_polymorphic_man
|
||||
assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Face.find(:first).horrible_polymorphic_man = Man.first }
|
||||
end
|
||||
|
||||
def test_trying_to_set_polymorphic_inverses_that_dont_exist_on_the_instance_being_set_should_raise_an_error
|
||||
# passes because Man does have the correct inverse_of
|
||||
assert_nothing_raised(ActiveRecord::InverseOfAssociationNotFoundError) { Face.find(:first).polymorphic_man = Man.first }
|
||||
# fails because Interest does have the correct inverse_of
|
||||
assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Face.find(:first).polymorphic_man = Interest.first }
|
||||
end
|
||||
end
|
||||
|
||||
# NOTE - these tests might not be meaningful, ripped as they were from the parental_control plugin
|
||||
# which would guess the inverse rather than look for an explicit configuration option.
|
||||
class InverseMultipleHasManyInversesForSameModel < ActiveRecord::TestCase
|
||||
fixtures :men, :interests, :zines
|
||||
|
||||
def test_that_we_can_load_associations_that_have_the_same_reciprocal_name_from_different_models
|
||||
assert_nothing_raised(ActiveRecord::AssociationTypeMismatch) do
|
||||
i = Interest.find(:first)
|
||||
z = i.zine
|
||||
m = i.man
|
||||
end
|
||||
end
|
||||
|
||||
def test_that_we_can_create_associations_that_have_the_same_reciprocal_name_from_different_models
|
||||
assert_nothing_raised(ActiveRecord::AssociationTypeMismatch) do
|
||||
i = Interest.find(:first)
|
||||
i.build_zine(:title => 'Get Some in Winter! 2008')
|
||||
i.build_man(:name => 'Gordon')
|
||||
i.save!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,712 +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 1, posts(:welcome)[:taggings_count]
|
||||
tagging = posts(:welcome).taggings.create(:tag => tags(:general))
|
||||
assert_equal 2, posts(:welcome, :reload)[:taggings_count]
|
||||
tagging.destroy
|
||||
assert_equal 1, posts(:welcome, :reload)[:taggings_count]
|
||||
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_equal Tagging.find(1,2).sort_by { |t| t.id }, 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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
def test_has_many_through_goes_through_all_sti_classes
|
||||
sub_sti_post = SubStiPost.create!(:title => 'test', :body => 'test', :author_id => 1)
|
||||
new_comment = sub_sti_post.comments.create(:body => 'test')
|
||||
|
||||
assert_equal [9, 10, new_comment.id], authors(:david).sti_post_comments.map(&:id).sort
|
||||
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,301 +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/ship'
|
||||
require 'models/ship_part'
|
||||
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, :people, :readers
|
||||
|
||||
def test_loading_the_association_target_should_keep_child_records_marked_for_destruction
|
||||
ship = Ship.create!(:name => "The good ship Dollypop")
|
||||
part = ship.parts.create!(:name => "Mast")
|
||||
part.mark_for_destruction
|
||||
ship.parts.send(:load_target)
|
||||
assert ship.parts[0].marked_for_destruction?
|
||||
end
|
||||
|
||||
def test_loading_the_association_target_should_load_most_recent_attributes_for_child_records_marked_for_destruction
|
||||
ship = Ship.create!(:name => "The good ship Dollypop")
|
||||
part = ship.parts.create!(:name => "Mast")
|
||||
part.mark_for_destruction
|
||||
ShipPart.find(part.id).update_attribute(:name, 'Deck')
|
||||
ship.parts.send(:load_target)
|
||||
assert_equal 'Deck', ship.parts[0].name
|
||||
end
|
||||
|
||||
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 person.readers.find(reader.id)
|
||||
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_force_reload_is_uncached
|
||||
firm = Firm.create!("name" => "A New Firm, Inc")
|
||||
client = Client.create!("name" => "TheClient.com", :firm => firm)
|
||||
ActiveRecord::Base.cache do
|
||||
firm.clients.each {}
|
||||
assert_queries(0) { assert_not_nil firm.clients.each {} }
|
||||
assert_queries(1) { assert_not_nil firm.clients(true).each {} }
|
||||
end
|
||||
end
|
||||
|
||||
def test_using_limitable_reflections_helper
|
||||
using_limitable_reflections = lambda { |reflections| ActiveRecord::Base.send :using_limitable_reflections?, reflections }
|
||||
belongs_to_reflections = [Tagging.reflect_on_association(:tag), Tagging.reflect_on_association(:super_tag)]
|
||||
has_many_reflections = [Tag.reflect_on_association(:taggings), Developer.reflect_on_association(:projects)]
|
||||
mixed_reflections = (belongs_to_reflections + has_many_reflections).uniq
|
||||
assert using_limitable_reflections.call(belongs_to_reflections), "Belong to associations are limitable"
|
||||
assert !using_limitable_reflections.call(has_many_reflections), "All has many style associations are not limitable"
|
||||
assert !using_limitable_reflections.call(mixed_reflections), "No collection associations (has many style) should pass"
|
||||
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,305 +0,0 @@
|
|||
require "cases/helper"
|
||||
require 'models/topic'
|
||||
require 'models/minimalistic'
|
||||
|
||||
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_typecast_attribute_from_select_to_false
|
||||
topic = Topic.create(:title => 'Budget')
|
||||
topic = Topic.find(:first, :select => "topics.*, 1=2 as is_test")
|
||||
assert !topic.is_test?
|
||||
end
|
||||
|
||||
def test_typecast_attribute_from_select_to_true
|
||||
topic = Topic.create(:title => 'Budget')
|
||||
topic = Topic.find(:first, :select => "topics.*, 2=2 as is_test")
|
||||
assert topic.is_test?
|
||||
end
|
||||
|
||||
def test_kernel_methods_not_implemented_in_activerecord
|
||||
%w(test name display y).each do |method|
|
||||
assert !ActiveRecord::Base.instance_method_already_implemented?(method), "##{method} is defined"
|
||||
end
|
||||
end
|
||||
|
||||
def test_primary_key_implemented
|
||||
assert 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 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 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_raise 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 ActiveSupport::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 ActiveSupport::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 ActiveSupport::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 ActiveSupport::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 ActiveSupport::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 ActiveSupport::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_conversion_for_attributes_should_write_value_on_class_variable
|
||||
Topic.skip_time_zone_conversion_for_attributes = [:field_a]
|
||||
Minimalistic.skip_time_zone_conversion_for_attributes = [:field_b]
|
||||
|
||||
assert_equal [:field_a], Topic.skip_time_zone_conversion_for_attributes
|
||||
assert_equal [:field_b], Minimalistic.skip_time_zone_conversion_for_attributes
|
||||
end
|
||||
|
||||
def test_read_attributes_respect_access_control
|
||||
privatize("title")
|
||||
|
||||
topic = @target.new(:title => "The pros and cons of programming naked.")
|
||||
assert !topic.respond_to?(:title)
|
||||
exception = assert_raise(NoMethodError) { topic.title }
|
||||
assert_equal "Attempt to call private method", exception.message
|
||||
assert_equal "I'm private", topic.send(:title)
|
||||
end
|
||||
|
||||
def test_write_attributes_respect_access_control
|
||||
privatize("title=(value)")
|
||||
|
||||
topic = @target.new
|
||||
assert !topic.respond_to?(:title=)
|
||||
exception = assert_raise(NoMethodError) { topic.title = "Pants"}
|
||||
assert_equal "Attempt to call private method", exception.message
|
||||
topic.send(:title=, "Very large pants")
|
||||
end
|
||||
|
||||
def test_question_attributes_respect_access_control
|
||||
privatize("title?")
|
||||
|
||||
topic = @target.new(:title => "Isaac Newton's pants")
|
||||
assert !topic.respond_to?(:title?)
|
||||
exception = assert_raise(NoMethodError) { topic.title? }
|
||||
assert_equal "Attempt to call private method", exception.message
|
||||
assert topic.send(:title?)
|
||||
end
|
||||
|
||||
def test_bulk_update_respects_access_control
|
||||
privatize("title=(value)")
|
||||
|
||||
assert_raise(ActiveRecord::UnknownAttributeError) { topic = @target.new(:title => "Rants about pants") }
|
||||
assert_raise(ActiveRecord::UnknownAttributeError) { @target.new.attributes = { :title => "Ants in pants" } }
|
||||
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 ? ActiveSupport::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
|
||||
|
||||
def privatize(method_signature)
|
||||
@target.class_eval <<-private_method
|
||||
private
|
||||
def #{method_signature}
|
||||
"I'm private"
|
||||
end
|
||||
private_method
|
||||
end
|
||||
end
|
||||
File diff suppressed because it is too large
Load diff
2143
vendor/rails/activerecord/test/cases/base_test.rb
vendored
2143
vendor/rails/activerecord/test/cases/base_test.rb
vendored
File diff suppressed because it is too large
Load diff
|
|
@ -1,81 +0,0 @@
|
|||
require 'cases/helper'
|
||||
require 'models/post'
|
||||
|
||||
class EachTest < ActiveRecord::TestCase
|
||||
fixtures :posts
|
||||
|
||||
def setup
|
||||
@posts = Post.all(:order => "id asc")
|
||||
@total = Post.count
|
||||
end
|
||||
|
||||
def test_each_should_excecute_one_query_per_batch
|
||||
assert_queries(Post.count + 1) do
|
||||
Post.find_each(:batch_size => 1) do |post|
|
||||
assert_kind_of Post, post
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_each_should_raise_if_the_order_is_set
|
||||
assert_raise(RuntimeError) do
|
||||
Post.find_each(:order => "title") { |post| post }
|
||||
end
|
||||
end
|
||||
|
||||
def test_each_should_raise_if_the_limit_is_set
|
||||
assert_raise(RuntimeError) do
|
||||
Post.find_each(:limit => 1) { |post| post }
|
||||
end
|
||||
end
|
||||
|
||||
def test_find_in_batches_should_return_batches
|
||||
assert_queries(Post.count + 1) do
|
||||
Post.find_in_batches(:batch_size => 1) do |batch|
|
||||
assert_kind_of Array, batch
|
||||
assert_kind_of Post, batch.first
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_find_in_batches_should_start_from_the_start_option
|
||||
assert_queries(Post.count) do
|
||||
Post.find_in_batches(:batch_size => 1, :start => 2) do |batch|
|
||||
assert_kind_of Array, batch
|
||||
assert_kind_of Post, batch.first
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_find_in_batches_shouldnt_excute_query_unless_needed
|
||||
post_count = Post.count
|
||||
|
||||
assert_queries(2) do
|
||||
Post.find_in_batches(:batch_size => post_count) {|batch| assert_kind_of Array, batch }
|
||||
end
|
||||
|
||||
assert_queries(1) do
|
||||
Post.find_in_batches(:batch_size => post_count + 1) {|batch| assert_kind_of Array, batch }
|
||||
end
|
||||
end
|
||||
|
||||
def test_find_in_batches_doesnt_clog_conditions
|
||||
Post.find_in_batches(:conditions => {:id => posts(:welcome).id}) do
|
||||
assert_nothing_raised { Post.find(posts(:thinking).id) }
|
||||
end
|
||||
end
|
||||
|
||||
def test_each_should_raise_if_select_is_set_without_id
|
||||
assert_raise(RuntimeError) do
|
||||
Post.find_each(:select => :title, :batch_size => 1) { |post| post }
|
||||
end
|
||||
end
|
||||
|
||||
def test_each_should_execute_if_id_is_in_select
|
||||
assert_queries(4) do
|
||||
Post.find_each(:select => "id, title, type", :batch_size => 2) do |post|
|
||||
assert_kind_of Post, post
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
require "cases/helper"
|
||||
|
||||
# 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?(: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,347 +0,0 @@
|
|||
require "cases/helper"
|
||||
require 'models/company'
|
||||
require 'models/topic'
|
||||
require 'models/edge'
|
||||
require 'models/owner'
|
||||
require 'models/pet'
|
||||
require 'models/toy'
|
||||
require 'models/club'
|
||||
require 'models/organization'
|
||||
|
||||
Company.has_many :accounts
|
||||
|
||||
class NumericData < ActiveRecord::Base
|
||||
self.table_name = 'numeric_data'
|
||||
end
|
||||
|
||||
class CalculationsTest < ActiveRecord::TestCase
|
||||
fixtures :companies, :accounts, :topics, :owners, :pets, :toys
|
||||
|
||||
def test_should_sum_field
|
||||
assert_equal 318, Account.sum(:credit_limit)
|
||||
end
|
||||
|
||||
def test_should_average_field
|
||||
value = Account.average(:credit_limit)
|
||||
assert_equal 53.0, value
|
||||
end
|
||||
|
||||
def test_should_return_nil_as_average
|
||||
assert_nil NumericData.average(:bank_balance)
|
||||
end
|
||||
|
||||
def test_type_cast_calculated_value_should_convert_db_averages_of_fixnum_class_to_decimal
|
||||
assert_equal 0, NumericData.send(:type_cast_calculated_value, 0, nil, 'avg')
|
||||
assert_equal 53.0, NumericData.send(:type_cast_calculated_value, 53, nil, 'avg')
|
||||
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_field_having_sanitized_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')
|
||||
assert_equal 0, companies(:rails_core).companies.sum(:id, :conditions => '1 = 2')
|
||||
end
|
||||
|
||||
def test_sum_should_return_valid_values_for_decimals
|
||||
NumericData.create(:bank_balance => 19.83)
|
||||
assert_equal 19.83, NumericData.sum(:bank_balance)
|
||||
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
|
||||
|
||||
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)
|
||||
first_key = c.keys.first
|
||||
assert_equal Firm, first_key.class
|
||||
assert_equal 1, c[first_key]
|
||||
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_from
|
||||
assert_equal Club.count, Organization.clubs.count
|
||||
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_raise(ArgumentError) { Company.send(:validate_calculation_options, :sum, :foo => :bar) }
|
||||
assert_raise(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_count_with_scoped_has_many_through_association
|
||||
assert_equal 1, owners(:blackbeard).toys.with_name('Bone').count
|
||||
end
|
||||
|
||||
def test_should_sum_expression
|
||||
assert_equal 636, Account.sum("2 * credit_limit").to_i
|
||||
end
|
||||
|
||||
def test_count_with_from_option
|
||||
assert_equal Company.count(:all), Company.count(:all, :from => 'companies')
|
||||
assert_equal Account.count(:all, :conditions => "credit_limit = 50"),
|
||||
Account.count(:all, :from => 'accounts', :conditions => "credit_limit = 50")
|
||||
assert_equal Company.count(:type, :conditions => {:type => "Firm"}),
|
||||
Company.count(:type, :conditions => {:type => "Firm"}, :from => 'companies')
|
||||
end
|
||||
|
||||
def test_sum_with_from_option
|
||||
assert_equal Account.sum(:credit_limit), Account.sum(:credit_limit, :from => 'accounts')
|
||||
assert_equal Account.sum(:credit_limit, :conditions => "credit_limit > 50"),
|
||||
Account.sum(:credit_limit, :from => 'accounts', :conditions => "credit_limit > 50")
|
||||
end
|
||||
|
||||
def test_average_with_from_option
|
||||
assert_equal Account.average(:credit_limit), Account.average(:credit_limit, :from => 'accounts')
|
||||
assert_equal Account.average(:credit_limit, :conditions => "credit_limit > 50"),
|
||||
Account.average(:credit_limit, :from => 'accounts', :conditions => "credit_limit > 50")
|
||||
end
|
||||
|
||||
def test_minimum_with_from_option
|
||||
assert_equal Account.minimum(:credit_limit), Account.minimum(:credit_limit, :from => 'accounts')
|
||||
assert_equal Account.minimum(:credit_limit, :conditions => "credit_limit > 50"),
|
||||
Account.minimum(:credit_limit, :from => 'accounts', :conditions => "credit_limit > 50")
|
||||
end
|
||||
|
||||
def test_maximum_with_from_option
|
||||
assert_equal Account.maximum(:credit_limit), Account.maximum(:credit_limit, :from => 'accounts')
|
||||
assert_equal Account.maximum(:credit_limit, :conditions => "credit_limit > 50"),
|
||||
Account.maximum(:credit_limit, :from => 'accounts', :conditions => "credit_limit > 50")
|
||||
end
|
||||
|
||||
def test_from_option_with_specified_index
|
||||
if Edge.connection.adapter_name == 'MySQL'
|
||||
assert_equal Edge.count(:all), Edge.count(:all, :from => 'edges USE INDEX(unique_edge_index)')
|
||||
assert_equal Edge.count(:all, :conditions => 'sink_id < 5'),
|
||||
Edge.count(:all, :from => 'edges USE INDEX(unique_edge_index)', :conditions => 'sink_id < 5')
|
||||
end
|
||||
end
|
||||
|
||||
def test_from_option_with_table_different_than_class
|
||||
assert_equal Account.count(:all), Company.count(:all, :from => 'accounts')
|
||||
end
|
||||
|
||||
end
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
require "cases/helper"
|
||||
|
||||
class Comment < ActiveRecord::Base
|
||||
attr_accessor :callers
|
||||
|
||||
before_validation :record_callers
|
||||
|
||||
def after_validation
|
||||
record_callers
|
||||
end
|
||||
|
||||
def record_callers
|
||||
callers << self.class if callers
|
||||
end
|
||||
end
|
||||
|
||||
class CommentObserver < ActiveRecord::Observer
|
||||
attr_accessor :callers
|
||||
|
||||
def after_validation(model)
|
||||
callers << self.class if callers
|
||||
end
|
||||
end
|
||||
|
||||
class CallbacksObserversTest < ActiveRecord::TestCase
|
||||
def test_model_callbacks_fire_before_observers_are_notified
|
||||
callers = []
|
||||
|
||||
comment = Comment.new
|
||||
comment.callers = callers
|
||||
|
||||
CommentObserver.instance.callers = callers
|
||||
|
||||
comment.valid?
|
||||
|
||||
assert_equal [Comment, Comment, CommentObserver], callers, "model callbacks did not fire before observers were notified"
|
||||
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