aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord
diff options
context:
space:
mode:
authorPratik Naik <pratiknaik@gmail.com>2009-08-31 22:11:50 +0100
committerPratik Naik <pratiknaik@gmail.com>2009-08-31 22:11:50 +0100
commitbae00bb1cc392e1cf408369809b9cf85468bef42 (patch)
tree17103af6eeb5de96c72beda1debce28950cc7fea /activerecord
parent93c76b2fb08668bc4b8364cc8051476e6d1d15ba (diff)
parentffd2cf167040b60c26d97c01598560c87bd4b2d3 (diff)
downloadrails-bae00bb1cc392e1cf408369809b9cf85468bef42.tar.gz
rails-bae00bb1cc392e1cf408369809b9cf85468bef42.tar.bz2
rails-bae00bb1cc392e1cf408369809b9cf85468bef42.zip
Merge commit 'mainstream/master'
Diffstat (limited to 'activerecord')
-rw-r--r--activerecord/CHANGELOG8
-rw-r--r--activerecord/Rakefile40
-rw-r--r--activerecord/examples/.gitignore1
-rwxr-xr-xactiverecord/examples/performance.rb162
-rw-r--r--activerecord/lib/active_record.rb1
-rwxr-xr-xactiverecord/lib/active_record/associations.rb31
-rw-r--r--activerecord/lib/active_record/associations/association_collection.rb1
-rw-r--r--activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb16
-rw-r--r--activerecord/lib/active_record/associations/has_many_association.rb1
-rw-r--r--activerecord/lib/active_record/associations/has_many_through_association.rb33
-rw-r--r--activerecord/lib/active_record/associations/has_one_through_association.rb12
-rw-r--r--activerecord/lib/active_record/associations/through_association_scope.rb2
-rw-r--r--activerecord/lib/active_record/attribute_methods/dirty.rb147
-rwxr-xr-xactiverecord/lib/active_record/base.rb28
-rw-r--r--activerecord/lib/active_record/calculations.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb15
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb4
-rwxr-xr-xactiverecord/lib/active_record/connection_adapters/abstract_adapter.rb7
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql_adapter.rb21
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb45
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb5
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb44
-rw-r--r--activerecord/lib/active_record/fixtures.rb16
-rw-r--r--activerecord/lib/active_record/locale/en.yml1
-rw-r--r--activerecord/lib/active_record/reflection.rb2
-rw-r--r--activerecord/lib/active_record/serialization.rb80
-rw-r--r--activerecord/lib/active_record/serializers/json_serializer.rb14
-rw-r--r--activerecord/lib/active_record/serializers/xml_serializer.rb19
-rw-r--r--activerecord/lib/active_record/validations.rb3
-rw-r--r--activerecord/lib/active_record/validations/uniqueness.rb2
-rw-r--r--activerecord/lib/active_record/validator.rb68
-rw-r--r--activerecord/test/cases/adapter_test.rb12
-rw-r--r--activerecord/test/cases/associations/belongs_to_associations_test.rb18
-rw-r--r--activerecord/test/cases/associations/habtm_join_table_test.rb56
-rw-r--r--activerecord/test/cases/associations/has_many_associations_test.rb58
-rw-r--r--activerecord/test/cases/associations/has_many_through_associations_test.rb55
-rw-r--r--activerecord/test/cases/associations/has_one_through_associations_test.rb10
-rw-r--r--activerecord/test/cases/associations/join_model_test.rb8
-rwxr-xr-xactiverecord/test/cases/base_test.rb65
-rw-r--r--activerecord/test/cases/column_definition_test.rb34
-rw-r--r--activerecord/test/cases/dirty_test.rb78
-rw-r--r--activerecord/test/cases/i18n_test.rb5
-rw-r--r--activerecord/test/cases/migration_test.rb50
-rw-r--r--activerecord/test/cases/named_scope_test.rb6
-rw-r--r--activerecord/test/cases/pk_test.rb18
-rw-r--r--activerecord/test/cases/reflection_test.rb4
-rw-r--r--activerecord/test/cases/schema_dumper_test.rb12
-rw-r--r--activerecord/test/cases/validations/i18n_validation_test.rb188
-rw-r--r--activerecord/test/cases/validations/uniqueness_validation_test.rb19
-rw-r--r--activerecord/test/connections/native_sqlite/connection.rb25
-rw-r--r--activerecord/test/fixtures/posts.yml3
-rw-r--r--activerecord/test/models/author.rb1
-rw-r--r--activerecord/test/models/comment.rb6
-rw-r--r--activerecord/test/models/company.rb2
-rw-r--r--activerecord/test/models/contract.rb4
-rw-r--r--activerecord/test/schema/postgresql_specific_schema.rb15
-rw-r--r--activerecord/test/schema/schema.rb4
57 files changed, 1190 insertions, 397 deletions
diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG
index 9adc6b887f..d8bfb1916d 100644
--- a/activerecord/CHANGELOG
+++ b/activerecord/CHANGELOG
@@ -1,5 +1,9 @@
*Edge*
+* Remove support for SQLite 2. Please upgrade to SQLite 3+ or install the plugin from git://github.com/rails/sqlite2_adapter.git [Pratik Naik]
+
+* PostgreSQL: XML datatype support. #1874 [Leonardo Borges]
+
* quoted_date converts time-like objects to ActiveRecord::Base.default_timezone before serialization. This allows you to use Time.now in find conditions and have it correctly be serialized as the current time in UTC when default_timezone == :utc. #2946 [Geoff Buesing]
* SQLite: drop support for 'dbfile' option in favor of 'database.' #2363 [Paul Hinze, Jeremy Kemper]
@@ -2197,7 +2201,7 @@ during calendar reform. #7649, #7724 [fedot, Geoff Buesing]
* Escape database name in MySQL adapter when creating and dropping databases. #3409 [anna@wota.jp]
-* Disambiguate table names for columns in validates_uniquness_of's WHERE clause. #3423 [alex.borovsky@gmail.com]
+* Disambiguate table names for columns in validates_uniqueness_of's WHERE clause. #3423 [alex.borovsky@gmail.com]
* .with_scope imposed create parameters now bypass attr_protected [Tobias Lütke]
@@ -3712,7 +3716,7 @@ in effect. Added :readonly finder constraint. Calling an association collectio
* Escape database name in MySQL adapter when creating and dropping databases. #3409 [anna@wota.jp]
-* Disambiguate table names for columns in validates_uniquness_of's WHERE clause. #3423 [alex.borovsky@gmail.com]
+* Disambiguate table names for columns in validates_uniqueness_of's WHERE clause. #3423 [alex.borovsky@gmail.com]
* .with_scope imposed create parameters now bypass attr_protected [Tobias Lütke]
diff --git a/activerecord/Rakefile b/activerecord/Rakefile
index 09dbc5ad6d..a9e4a92e37 100644
--- a/activerecord/Rakefile
+++ b/activerecord/Rakefile
@@ -24,19 +24,39 @@ 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 => defined?(JRUBY_VERSION) ?
- %w(test_jdbcmysql test_jdbcsqlite3 test_jdbcpostgresql) :
- %w(test_mysql test_sqlite3 test_postgresql)
-task :isolated_test => defined?(JRUBY_VERSION) ?
- %w(isolated_test_jdbcmysql isolated_test_jdbcsqlite3 isolated_test_jdbcpostgresql) :
- %w(isolated_test_mysql isolated_test_sqlite3 isolated_test_postgresql)
-
-%w( mysql postgresql sqlite sqlite3 firebird db2 oracle sybase openbase frontbase jdbcmysql jdbcpostgresql jdbcsqlite3 jdbcderby jdbch2 jdbchsqldb ).each do |adapter|
+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
+
+task :isolated_test do
+ tasks = defined?(JRUBY_VERSION) ?
+ %w(isolated_test_jdbcmysql isolated_test_jdbcsqlite3 isolated_test_jdbcpostgresql) :
+ %w(isolated_test_mysql isolated_test_sqlite3 isolated_test_postgresql)
+ run_without_aborting(*tasks)
+end
+
+%w( mysql postgresql sqlite3 firebird db2 oracle sybase openbase frontbase jdbcmysql jdbcpostgresql jdbcsqlite3 jdbcderby jdbch2 jdbchsqldb ).each do |adapter|
Rake::TestTask.new("test_#{adapter}") { |t|
connection_path = "test/connections/#{adapter =~ /jdbc/ ? 'jdbc' : 'native'}_#{adapter}"
adapter_short = adapter == 'db2' ? adapter : adapter[/^[a-z]+/]
@@ -86,8 +106,8 @@ task :rebuild_mysql_databases => 'mysql:rebuild_databases'
namespace :postgresql do
desc 'Build the PostgreSQL test databases'
task :build_databases do
- %x( createdb activerecord_unittest )
- %x( createdb activerecord_unittest2 )
+ %x( createdb -E UTF8 activerecord_unittest )
+ %x( createdb -E UTF8 activerecord_unittest2 )
end
desc 'Drop the PostgreSQL test databases'
diff --git a/activerecord/examples/.gitignore b/activerecord/examples/.gitignore
new file mode 100644
index 0000000000..0dfc1cb7fb
--- /dev/null
+++ b/activerecord/examples/.gitignore
@@ -0,0 +1 @@
+performance.sql
diff --git a/activerecord/examples/performance.rb b/activerecord/examples/performance.rb
new file mode 100755
index 0000000000..53acd62f47
--- /dev/null
+++ b/activerecord/examples/performance.rb
@@ -0,0 +1,162 @@
+#!/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"
+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"
diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb
index 68b0251982..2f1e7573d8 100644
--- a/activerecord/lib/active_record.rb
+++ b/activerecord/lib/active_record.rb
@@ -69,6 +69,7 @@ module ActiveRecord
autoload :TestCase, 'active_record/test_case'
autoload :Timestamp, 'active_record/timestamp'
autoload :Transactions, 'active_record/transactions'
+ autoload :Validator, 'active_record/validator'
autoload :Validations, 'active_record/validations'
module AttributeMethods
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index 7f299b2aa5..f494e38e2f 100755
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -42,11 +42,12 @@ module ActiveRecord
end
end
- class HasManyThroughCantAssociateThroughHasManyReflection < ActiveRecordError #:nodoc:
+ class HasManyThroughCantAssociateThroughHasOneOrManyReflection < ActiveRecordError #:nodoc:
def initialize(owner, reflection)
super("Cannot modify association '#{owner.class.name}##{reflection.name}' because the source reflection class '#{reflection.source_reflection.class_name}' is associated to '#{reflection.through_reflection.class_name}' via :#{reflection.source_reflection.macro}.")
end
end
+
class HasManyThroughCantAssociateNewRecords < ActiveRecordError #:nodoc:
def initialize(owner, reflection)
super("Cannot associate new records through '#{owner.class.name}##{reflection.name}' on '#{reflection.source_reflection.class_name rescue nil}##{reflection.source_reflection.name rescue nil}'. Both records must have an id in order to create the has_many :through record associating them.")
@@ -416,6 +417,32 @@ module ActiveRecord
# @firm.clients.collect { |c| c.invoices }.flatten # select all invoices for all clients of the firm
# @firm.invoices # selects all invoices by going through the Client join model.
#
+ # Similarly you can go through a +has_one+ association on the join model:
+ #
+ # class Group < ActiveRecord::Base
+ # has_many :users
+ # has_many :avatars, :through => :users
+ # end
+ #
+ # class User < ActiveRecord::Base
+ # belongs_to :group
+ # has_one :avatar
+ # end
+ #
+ # class Avatar < ActiveRecord::Base
+ # belongs_to :user
+ # end
+ #
+ # @group = Group.first
+ # @group.users.collect { |u| u.avatar }.flatten # select all avatars for all users in the group
+ # @group.avatars # selects all avatars by going through the User join model.
+ #
+ # An important caveat with going through +has_one+ or +has_many+ associations on the join model is that these associations are
+ # *read-only*. For example, the following would not work following the previous example:
+ #
+ # @group.avatars << Avatar.new # this would work if User belonged_to Avatar rather than the other way around.
+ # @group.avatars.delete(@group.avatars.last) # so would this
+ #
# === Polymorphic Associations
#
# Polymorphic associations on models are not restricted on what types of models they can be associated with. Rather, they
@@ -819,7 +846,7 @@ module ActiveRecord
# [:through]
# Specifies a Join Model through which to perform the query. Options for <tt>:class_name</tt> and <tt>:foreign_key</tt>
# are ignored, as the association uses the source reflection. You can only use a <tt>:through</tt> query through a <tt>belongs_to</tt>
- # or <tt>has_many</tt> association on the join model.
+ # <tt>has_one</tt> or <tt>has_many</tt> association on the join model.
# [:source]
# Specifies the source association name used by <tt>has_many :through</tt> queries. Only use it if the name cannot be
# inferred from the association. <tt>has_many :subscribers, :through => :subscriptions</tt> will look for either <tt>:subscribers</tt> or
diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb
index e67ccfb228..1b7bf42b91 100644
--- a/activerecord/lib/active_record/associations/association_collection.rb
+++ b/activerecord/lib/active_record/associations/association_collection.rb
@@ -208,6 +208,7 @@ module ActiveRecord
# 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
diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb
index fd23e59e82..d91c555dad 100644
--- a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb
+++ b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb
@@ -1,6 +1,11 @@
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
@@ -17,6 +22,12 @@ module ActiveRecord
@reflection.reset_column_information
end
+ def has_primary_key?
+ return @has_primary_key unless @has_primary_key.nil?
+ @has_primary_key = (ActiveRecord::Base.connection.supports_primary_key? &&
+ ActiveRecord::Base.connection.primary_key(@reflection.options[:join_table]))
+ end
+
protected
def construct_find_options!(options)
options[:joins] = @join_sql
@@ -29,6 +40,11 @@ module ActiveRecord
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!
diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb
index e4b631bc54..73d3c23cd3 100644
--- a/activerecord/lib/active_record/associations/has_many_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_association.rb
@@ -74,6 +74,7 @@ module ActiveRecord
"#{@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
diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb
index f4507c979c..829f0ac0c5 100644
--- a/activerecord/lib/active_record/associations/has_many_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_through_association.rb
@@ -8,21 +8,11 @@ module ActiveRecord
alias_method :new, :build
def create!(attrs = nil)
- ensure_owner_is_not_new
-
- transaction do
- self << (object = attrs ? @reflection.klass.send(:with_scope, :create => attrs) { @reflection.create_association! } : @reflection.create_association!)
- object
- end
+ create_record(attrs, true)
end
def create(attrs = nil)
- ensure_owner_is_not_new
-
- transaction do
- self << (object = attrs ? @reflection.klass.send(:with_scope, :create => attrs) { @reflection.create_association } : @reflection.create_association)
- object
- end
+ create_record(attrs, false)
end
def destroy(*records)
@@ -40,8 +30,18 @@ module ActiveRecord
return @target.size if loaded?
return count
end
-
+
protected
+ def create_record(attrs, force = true)
+ ensure_owner_is_not_new
+
+ transaction do
+ object = @reflection.klass.new(attrs)
+ add_record_to_target_with_callbacks(object) {|r| insert_record(object, force) }
+ object
+ end
+ end
+
def target_reflection_has_associated_record?
if @reflection.through_reflection.macro == :belongs_to && @owner[@reflection.through_reflection.primary_key_name].blank?
false
@@ -65,9 +65,10 @@ module ActiveRecord
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! }
+
+ through_association = @owner.send(@reflection.through_reflection.name)
+ through_record = through_association.create!(construct_join_attributes(record))
+ through_association.proxy_target << through_record
end
# TODO - add dependent option support
diff --git a/activerecord/lib/active_record/associations/has_one_through_association.rb b/activerecord/lib/active_record/associations/has_one_through_association.rb
index 830aa1808a..a79bf943d1 100644
--- a/activerecord/lib/active_record/associations/has_one_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_one_through_association.rb
@@ -18,9 +18,15 @@ module ActiveRecord
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
- else
- @owner.send(@reflection.through_reflection.name, klass.send(:create, construct_join_attributes(new_value))) if new_value
+ 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
diff --git a/activerecord/lib/active_record/associations/through_association_scope.rb b/activerecord/lib/active_record/associations/through_association_scope.rb
index 8e7ce33814..16b6123439 100644
--- a/activerecord/lib/active_record/associations/through_association_scope.rb
+++ b/activerecord/lib/active_record/associations/through_association_scope.rb
@@ -93,7 +93,7 @@ module ActiveRecord
# Construct attributes for :through pointing to owner and associate.
def construct_join_attributes(associate)
# TODO: revist this to allow it for deletion, supposing dependent option is supported
- raise ActiveRecord::HasManyThroughCantAssociateThroughHasManyReflection.new(@owner, @reflection) if @reflection.source_reflection.macro == :has_many
+ 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)
diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb
index 911c908c8b..4df0f1af69 100644
--- a/activerecord/lib/active_record/attribute_methods/dirty.rb
+++ b/activerecord/lib/active_record/attribute_methods/dirty.rb
@@ -1,95 +1,25 @@
+require 'active_support/core_ext/object/tap'
+
module ActiveRecord
module AttributeMethods
- # 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'] }
- #
- # Resetting an attribute returns it to its original state:
- # person.reset_name! # => 'Bill'
- # person.changed? # => false
- # person.name_changed? # => false
- # person.name # => 'Bill'
- #
- # Before modifying an attribute in-place:
- # person.name_will_change!
- # person.name << 'y'
- # person.name_change # => ['Bill', 'Billy']
module Dirty
extend ActiveSupport::Concern
-
- DIRTY_AFFIXES = [
- { :suffix => '_changed?' },
- { :suffix => '_change' },
- { :suffix => '_will_change!' },
- { :suffix => '_was' },
- { :prefix => 'reset_', :suffix => '!' }
- ]
+ include ActiveModel::Dirty
included do
- attribute_method_affix *DIRTY_AFFIXES
-
- alias_method_chain :save, :dirty
- alias_method_chain :save!, :dirty
- alias_method_chain :update, :dirty
- alias_method_chain :reload, :dirty
+ alias_method_chain :save, :dirty
+ alias_method_chain :save!, :dirty
+ alias_method_chain :update, :dirty
+ alias_method_chain :reload, :dirty
superclass_delegating_accessor :partial_updates
self.partial_updates = true
end
- # Do any attributes have unsaved changes?
- # person.changed? # => false
- # person.name = 'bob'
- # person.changed? # => true
- def changed?
- !changed_attributes.empty?
- end
-
- # List of attributes with unsaved changes.
- # person.changed # => []
- # person.name = 'bob'
- # person.changed # => ['name']
- def changed
- changed_attributes.keys
- end
-
- # Map of changed attrs => [original value, new value].
- # person.changes # => {}
- # person.name = 'bob'
- # person.changes # => { 'name' => ['bill', 'bob'] }
- def changes
- changed.inject({}) { |h, attr| h[attr] = attribute_change(attr); h }
- end
-
# Attempts to +save+ the record and clears changed attributes if successful.
def save_with_dirty(*args) #:nodoc:
if status = save_without_dirty(*args)
+ @previously_changed = changes
changed_attributes.clear
end
status
@@ -97,49 +27,21 @@ module ActiveRecord
# 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
+ save_without_dirty!(*args).tap do
+ @previously_changed = changes
+ changed_attributes.clear
+ end
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
+ reload_without_dirty(*args).tap do
+ previously_changed_attributes.clear
+ changed_attributes.clear
+ end
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>reset_*!</tt> for +method_missing+.
- def reset_attribute!(attr)
- self[attr] = changed_attributes[attr] if attribute_changed?(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(attr, value)
attr = attr.to_s
@@ -182,23 +84,6 @@ module ActiveRecord
old != value
end
-
- module ClassMethods
- def self.extended(base)
- class << base
- alias_method_chain :alias_attribute, :dirty
- end
- end
-
- def alias_attribute_with_dirty(new_name, old_name)
- alias_attribute_without_dirty(new_name, old_name)
- DIRTY_AFFIXES.each do |affixes|
- module_eval <<-STR, __FILE__, __LINE__+1
- def #{affixes[:prefix]}#{new_name}#{affixes[:suffix]}; self.#{affixes[:prefix]}#{old_name}#{affixes[:suffix]}; end # def reset_subject!; self.reset_title!; end
- STR
- end
- end
- end
end
end
end
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index 531a698f77..c5be561ea3 100755
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -1394,7 +1394,7 @@ module ActiveRecord #:nodoc:
end
defaults << options[:default] if options[:default]
defaults.flatten!
- defaults << attribute_key_name.humanize
+ defaults << attribute_key_name.to_s.humanize
options[:count] ||= 1
I18n.translate(defaults.shift, options.merge(:default => defaults, :scope => [:activerecord, :attributes]))
end
@@ -2294,20 +2294,24 @@ module ActiveRecord #:nodoc:
# And for value objects on a composed_of relationship:
# { :address => Address.new("123 abc st.", "chicago") }
# # => "address_street='123 abc st.' and address_city='chicago'"
- def sanitize_sql_hash_for_conditions(attrs, table_name = quoted_table_name)
+ def sanitize_sql_hash_for_conditions(attrs, default_table_name = quoted_table_name)
attrs = expand_hash_conditions_for_aggregates(attrs)
conditions = attrs.map do |attr, value|
+ table_name = default_table_name
+
unless value.is_a?(Hash)
attr = attr.to_s
# Extract table name from qualified attribute names.
if attr.include?('.')
- table_name, attr = attr.split('.', 2)
- table_name = connection.quote_table_name(table_name)
+ attr_table_name, attr = attr.split('.', 2)
+ attr_table_name = connection.quote_table_name(attr_table_name)
+ else
+ attr_table_name = table_name
end
- attribute_condition("#{table_name}.#{connection.quote_column_name(attr)}", value)
+ attribute_condition("#{attr_table_name}.#{connection.quote_column_name(attr)}", value)
else
sanitize_sql_hash_for_conditions(value, connection.quote_table_name(attr.to_s))
end
@@ -3013,16 +3017,22 @@ module ActiveRecord #:nodoc:
def execute_callstack_for_multiparameter_attributes(callstack)
errors = []
- callstack.each do |name, values|
+ callstack.each do |name, values_with_empty_parameters|
begin
klass = (self.class.reflect_on_aggregation(name.to_sym) || column_for_attribute(name)).klass
+ # in order to allow a date to be set without a year, we must keep the empty values.
+ # Otherwise, we wouldn't be able to distinguish it from a date with an empty day.
+ values = values_with_empty_parameters.reject(&:nil?)
+
if values.empty?
send(name + "=", nil)
else
+
value = if Time == klass
instantiate_time_object(name, values)
elsif Date == klass
begin
+ values = values_with_empty_parameters.collect do |v| v.nil? ? 1 : v end
Date.new(*values)
rescue ArgumentError => ex # if Date.new raises an exception on an invalid date
instantiate_time_object(name, values).to_date # we instantiate Time object and convert it back to a date thus using Time's logic in handling invalid dates
@@ -3050,10 +3060,8 @@ module ActiveRecord #:nodoc:
attribute_name = multiparameter_name.split("(").first
attributes[attribute_name] = [] unless attributes.include?(attribute_name)
- unless value.empty?
- attributes[attribute_name] <<
- [ find_parameter_position(multiparameter_name), type_cast_attribute_value(multiparameter_name, value) ]
- end
+ parameter_value = value.empty? ? nil : type_cast_attribute_value(multiparameter_name, value)
+ attributes[attribute_name] << [ find_parameter_position(multiparameter_name), parameter_value ]
end
attributes.each { |name, values| attributes[name] = values.sort_by{ |v| v.first }.collect { |v| v.last } }
diff --git a/activerecord/lib/active_record/calculations.rb b/activerecord/lib/active_record/calculations.rb
index 4a88c43dff..646fed1a0b 100644
--- a/activerecord/lib/active_record/calculations.rb
+++ b/activerecord/lib/active_record/calculations.rb
@@ -197,7 +197,7 @@ module ActiveRecord
sql << ", #{options[:group_field]} AS #{options[:group_alias]}" if options[:group]
if options[:from]
sql << " FROM #{options[:from]} "
- elsif scope && scope[:from]
+ elsif scope && scope[:from] && !use_workaround
sql << " FROM #{scope[:from]} "
else
sql << " FROM (SELECT #{distinct}#{column_name}" if use_workaround
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
index f346e3ebc8..520f3c8c0c 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
@@ -315,6 +315,20 @@ module ActiveRecord
@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)
@@ -705,3 +719,4 @@ module ActiveRecord
end
end
+
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
index 2473c772e3..20787ec510 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
@@ -107,7 +107,7 @@ module ActiveRecord
# See also TableDefinition#column for details on how to create columns.
def create_table(table_name, options = {})
table_definition = TableDefinition.new(self)
- table_definition.primary_key(options[:primary_key] || Base.get_primary_key(table_name)) unless options[:id] == false
+ 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?
@@ -329,7 +329,7 @@ module ActiveRecord
schema_migrations_table.column :version, :string, :null => false
end
add_index sm_table, :version, :unique => true,
- :name => 'unique_schema_migrations'
+ :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:
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
index c533d4cdb6..fab70f34b9 100755
--- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -56,6 +56,13 @@ module ActiveRecord
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?
diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
index 2b882a1f25..4edb64c2c0 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
@@ -53,12 +53,7 @@ module ActiveRecord
socket = config[:socket]
username = config[:username] ? config[:username].to_s : 'root'
password = config[:password].to_s
-
- if config.has_key?(:database)
- database = config[:database]
- else
- raise ArgumentError, "No database specified. Missing argument: database."
- end
+ database = config[:database]
# Require the MySQL driver and define Mysql::Result.all_hashes
unless defined? Mysql
@@ -81,7 +76,7 @@ module ActiveRecord
module ConnectionAdapters
class MysqlColumn < Column #:nodoc:
def extract_default(default)
- if type == :binary || type == :text
+ if sql_type =~ /blob/i || type == :text
if default.blank?
return null ? nil : ''
else
@@ -95,7 +90,7 @@ module ActiveRecord
end
def has_default?
- return false if type == :binary || type == :text #mysql forbids defaults on blob and text columns
+ return false if sql_type =~ /blob/i || type == :text #mysql forbids defaults on blob and text columns
super
end
@@ -213,6 +208,10 @@ module ActiveRecord
true
end
+ def supports_primary_key? #:nodoc:
+ true
+ end
+
def supports_savepoints? #:nodoc:
true
end
@@ -555,6 +554,12 @@ module ActiveRecord
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
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
index e77ae93c21..84cf1ad9fd 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -40,6 +40,12 @@ module ActiveRecord
end
module ConnectionAdapters
+ class TableDefinition
+ def xml(*args)
+ options = args.extract_options!
+ column(args[0], 'xml', options)
+ end
+ end
# PostgreSQL-specific extensions to column definitions in a table.
class PostgreSQLColumn < Column #:nodoc:
# Instantiates a new PostgreSQL column definition in a table.
@@ -68,7 +74,7 @@ module ActiveRecord
# depending on the server specifics
super
end
-
+
# Maps PostgreSQL-specific data types to logical Rails types.
def simplified_type(field_type)
case field_type
@@ -100,10 +106,10 @@ module ActiveRecord
:string
# XML type
when /^xml$/
- :string
+ :xml
# Arrays
when /^\D+\[\]$/
- :string
+ :string
# Object identifier types
when /^oid$/
:integer
@@ -112,7 +118,7 @@ module ActiveRecord
super
end
end
-
+
# Extracts the value from a PostgreSQL column default definition.
def self.extract_value_from_default(default)
case default
@@ -195,7 +201,8 @@ module ActiveRecord
:time => { :name => "time" },
:date => { :name => "date" },
:binary => { :name => "bytea" },
- :boolean => { :name => "boolean" }
+ :boolean => { :name => "boolean" },
+ :xml => { :name => "xml" }
}
# Returns 'PostgreSQL' as adapter name for identification purposes.
@@ -250,6 +257,11 @@ module ActiveRecord
true
end
+ # Does PostgreSQL support finding primary key on non-ActiveRecord tables?
+ def supports_primary_key? #:nodoc:
+ true
+ end
+
# Does PostgreSQL support standard conforming strings?
def supports_standard_conforming_strings?
# Temporarily set the client message level above error to prevent unintentional
@@ -273,7 +285,7 @@ module ActiveRecord
def supports_ddl_transactions?
true
end
-
+
def supports_savepoints?
true
end
@@ -365,7 +377,7 @@ module ActiveRecord
if value.kind_of?(String) && column && column.type == :binary
"#{quoted_string_prefix}'#{escape_bytea(value)}'"
elsif value.kind_of?(String) && column && column.sql_type =~ /^xml$/
- "xml '#{quote_string(value)}'"
+ "xml E'#{quote_string(value)}'"
elsif value.kind_of?(Numeric) && column && column.sql_type =~ /^money$/
# Not truly string input, so doesn't require (or allow) escape string syntax.
"'#{value.to_s}'"
@@ -564,7 +576,7 @@ module ActiveRecord
def rollback_db_transaction
execute "ROLLBACK"
end
-
+
if defined?(PGconn::PQTRANS_IDLE)
# The ruby-pg driver supports inspecting the transaction status,
# while the ruby-postgres driver does not.
@@ -811,6 +823,12 @@ module ActiveRecord
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
+
# Renames a table.
def rename_table(name, new_name)
execute "ALTER TABLE #{quote_table_name(name)} RENAME TO #{quote_table_name(new_name)}"
@@ -909,18 +927,18 @@ module ActiveRecord
sql = "DISTINCT ON (#{columns}) #{columns}, "
sql << order_columns * ', '
end
-
+
# Returns an ORDER BY clause for the passed order option.
- #
+ #
# PostgreSQL does not allow arbitrary ordering when using DISTINCT ON, so we work around this
# by wrapping the +sql+ string as a sub-select and ordering in that query.
def add_order_by_for_association_limiting!(sql, options) #:nodoc:
return sql if options[:order].blank?
-
+
order = options[:order].split(',').collect { |s| s.strip }.reject(&:blank?)
order.map! { |s| 'DESC' if s =~ /\bdesc$/i }
order = order.zip((0...order.size).to_a).map { |s,i| "id_list.alias_#{i} #{s}" }.join(', ')
-
+
sql.replace "SELECT * FROM (#{sql}) AS id_list ORDER BY #{order}"
end
@@ -1044,7 +1062,7 @@ module ActiveRecord
if res.ftype(cell_index) == MONEY_COLUMN_TYPE_OID
# Because money output is formatted according to the locale, there are two
# cases to consider (note the decimal separators):
- # (1) $12,345,678.12
+ # (1) $12,345,678.12
# (2) $12.345.678,12
case column = row[cell_index]
when /^-?\D+[\d,]+\.\d{2}$/ # (1)
@@ -1104,3 +1122,4 @@ module ActiveRecord
end
end
end
+
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
index 5eef692d05..d933bc924d 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
@@ -24,11 +24,6 @@ module ActiveRecord
module ConnectionAdapters #:nodoc:
class SQLite3Adapter < SQLiteAdapter # :nodoc:
- def table_structure(table_name)
- structure = @connection.table_info(quote_table_name(table_name))
- raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if structure.empty?
- structure
- end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
index c0f5046bff..5ed7094169 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
@@ -4,27 +4,6 @@ require 'active_support/core_ext/kernel/requires'
module ActiveRecord
class Base
class << self
- # Establishes a connection to the database that's used by all Active Record objects
- def sqlite_connection(config) # :nodoc:
- parse_sqlite_config!(config)
-
- unless self.class.const_defined?(:SQLite)
- require_library_or_gem(config[:adapter])
-
- db = SQLite::Database.new(config[:database], 0)
- db.show_datatypes = "ON" if !defined? SQLite::Version
- db.results_as_hash = true if defined? SQLite::Version
- db.type_translation = false
-
- # "Downgrade" deprecated sqlite API
- if SQLite.const_defined?(:Version)
- ConnectionAdapters::SQLite2Adapter.new(db, logger, config)
- else
- ConnectionAdapters::DeprecatedSQLiteAdapter.new(db, logger, config)
- end
- end
- end
-
private
def parse_sqlite_config!(config)
# Require database.
@@ -101,6 +80,10 @@ module ActiveRecord
true
end
+ def supports_primary_key? #:nodoc:
+ true
+ end
+
def requires_reloading?
true
end
@@ -324,9 +307,9 @@ module ActiveRecord
end
def table_structure(table_name)
- returning structure = execute("PRAGMA table_info(#{quote_table_name(table_name)})") do
- raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if structure.empty?
- end
+ structure = @connection.table_info(quote_table_name(table_name))
+ raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if structure.empty?
+ structure
end
def alter_table(table_name, options = {}) #:nodoc:
@@ -441,18 +424,5 @@ module ActiveRecord
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
diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb
index 6eeeddc9e1..99b812b5fc 100644
--- a/activerecord/lib/active_record/fixtures.rb
+++ b/activerecord/lib/active_record/fixtures.rb
@@ -622,7 +622,8 @@ class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash)
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)
+ association.association_foreign_key => Fixtures.identify(target) },
+ nil, @connection)
end
end
end
@@ -706,12 +707,12 @@ class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash)
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|
+ 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)
+ self[name] = Fixture.new(data, model_class, @connection)
end
end
end
@@ -724,7 +725,7 @@ class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash)
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)
+ self["#{@class_name.to_s.underscore}_#{i+=1}"] = Fixture.new(data, model_class, @connection)
end
end
@@ -762,7 +763,8 @@ class Fixture #:nodoc:
attr_reader :model_class
- def initialize(fixture, 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
@@ -784,14 +786,14 @@ class Fixture #:nodoc:
end
def key_list
- columns = @fixture.keys.collect{ |column_name| ActiveRecord::Base.connection.quote_column_name(column_name) }
+ 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 << ActiveRecord::Base.connection.quote(value, col).gsub('[^\]\\n', "\n").gsub('[^\]\\r', "\r")
+ fixtures << @connection.quote(value, col).gsub('[^\]\\n', "\n").gsub('[^\]\\r', "\r")
end
list * ', '
end
diff --git a/activerecord/lib/active_record/locale/en.yml b/activerecord/lib/active_record/locale/en.yml
index bf8a71d236..092f5f0023 100644
--- a/activerecord/lib/active_record/locale/en.yml
+++ b/activerecord/lib/active_record/locale/en.yml
@@ -23,6 +23,7 @@ en:
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.
# You can define own errors for models or model attributes.
diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb
index 0baa9654b7..db5d2b25ed 100644
--- a/activerecord/lib/active_record/reflection.rb
+++ b/activerecord/lib/active_record/reflection.rb
@@ -314,7 +314,7 @@ module ActiveRecord
raise HasManyThroughAssociationPolymorphicError.new(active_record.name, self, source_reflection)
end
- unless [:belongs_to, :has_many].include?(source_reflection.macro) && source_reflection.options[:through].nil?
+ unless [:belongs_to, :has_many, :has_one].include?(source_reflection.macro) && source_reflection.options[:through].nil?
raise HasManyThroughSourceAssociationMacroError.new(self)
end
diff --git a/activerecord/lib/active_record/serialization.rb b/activerecord/lib/active_record/serialization.rb
index 94f1e8f1fd..b49471f7ab 100644
--- a/activerecord/lib/active_record/serialization.rb
+++ b/activerecord/lib/active_record/serialization.rb
@@ -1,60 +1,58 @@
module ActiveRecord #:nodoc:
module Serialization
- module RecordSerializer #:nodoc:
- def initialize(*args)
- super
- options[:except] |= Array.wrap(@serializable.class.inheritance_column)
+ extend ActiveSupport::Concern
+ include ActiveModel::Serializers::JSON
+
+ def serializable_hash(options = nil)
+ options ||= {}
+
+ options[:except] = Array.wrap(options[:except]).map { |n| n.to_s }
+ options[:except] |= Array.wrap(self.class.inheritance_column)
+
+ hash = super(options)
+
+ serializable_add_includes(options) do |association, records, opts|
+ hash[association] = records.is_a?(Enumerable) ?
+ records.map { |r| r.serializable_hash(opts) } :
+ records.serializable_hash(opts)
end
+ hash
+ end
+
+ private
# 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.wrap(include_associations)
-
- for association in associations
- records = case @serializable.class.reflect_on_association(association).macro
- when :has_many, :has_and_belongs_to_many
- @serializable.send(association).to_a
- when :has_one, :belongs_to
- @serializable.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
+ def serializable_add_includes(options = {})
+ return unless include_associations = options.delete(:include)
- options[:include] = include_associations
- end
- end
+ 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.wrap(include_associations)
- def serializable_hash
- hash = super
+ for association in associations
+ records = case self.class.reflect_on_association(association).macro
+ when :has_many, :has_and_belongs_to_many
+ send(association).to_a
+ when :has_one, :belongs_to
+ send(association)
+ end
- add_includes do |association, records, opts|
- hash[association] =
- if records.is_a?(Enumerable)
- records.collect { |r| self.class.new(r, opts).serializable_hash }
- else
- self.class.new(records, opts).serializable_hash
- 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
- hash
+ options[:include] = include_associations
end
- end
end
end
require 'active_record/serializers/xml_serializer'
-require 'active_record/serializers/json_serializer'
diff --git a/activerecord/lib/active_record/serializers/json_serializer.rb b/activerecord/lib/active_record/serializers/json_serializer.rb
deleted file mode 100644
index 63bf42c09d..0000000000
--- a/activerecord/lib/active_record/serializers/json_serializer.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-module ActiveRecord #:nodoc:
- module Serialization
- extend ActiveSupport::Concern
- include ActiveModel::Serializers::JSON
-
- class JSONSerializer < ActiveModel::Serializers::JSON::Serializer
- include Serialization::RecordSerializer
- end
-
- def encode_json(encoder)
- JSONSerializer.new(self, encoder.options).to_s
- end
- end
-end
diff --git a/activerecord/lib/active_record/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb
index 4e172bd2b6..b19920741e 100644
--- a/activerecord/lib/active_record/serializers/xml_serializer.rb
+++ b/activerecord/lib/active_record/serializers/xml_serializer.rb
@@ -2,6 +2,8 @@ require 'active_support/core_ext/hash/conversions'
module ActiveRecord #:nodoc:
module Serialization
+ include ActiveModel::Serializers::Xml
+
# Builds an XML document to represent the model. Some configuration is
# available through +options+. However more complicated cases should
# override ActiveRecord::Base#to_xml.
@@ -169,18 +171,15 @@ module ActiveRecord #:nodoc:
# 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
+ XmlSerializer.new(self, options).serialize(&block)
end
end
class XmlSerializer < ActiveModel::Serializers::Xml::Serializer #:nodoc:
- include Serialization::RecordSerializer
+ def initialize(*args)
+ super
+ options[:except] |= Array.wrap(@serializable.class.inheritance_column)
+ end
def serializable_attributes
serializable_attribute_names.collect { |name| Attribute.new(name, @serializable) }
@@ -235,7 +234,9 @@ module ActiveRecord #:nodoc:
builder.tag!(*args) do
add_attributes
procs = options.delete(:procs)
- add_includes { |association, records, opts| add_associations(association, records, opts) }
+ @serializable.send(:serializable_add_includes, options) { |association, records, opts|
+ add_associations(association, records, opts)
+ }
options[:procs] = procs
add_procs
yield builder if block_given?
diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb
index a7fa98756e..5fc41cf054 100644
--- a/activerecord/lib/active_record/validations.rb
+++ b/activerecord/lib/active_record/validations.rb
@@ -12,7 +12,8 @@ module ActiveRecord
attr_reader :record
def initialize(record)
@record = record
- super("Validation failed: #{@record.errors.full_messages.join(", ")}")
+ errors = @record.errors.full_messages.join(I18n.t('support.array.words_connector', :default => ', '))
+ super(I18n.t('activerecord.errors.messages.record_invalid', :errors => errors))
end
end
diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb
index edec4e9e43..711086dc2c 100644
--- a/activerecord/lib/active_record/validations/uniqueness.rb
+++ b/activerecord/lib/active_record/validations/uniqueness.rb
@@ -119,7 +119,7 @@ module ActiveRecord
comparison_operator = "IS ?"
elsif column.text?
comparison_operator = "#{connection.case_sensitive_equality_operator} ?"
- value = column.limit ? value.to_s[0, column.limit] : value.to_s
+ value = column.limit ? value.to_s.mb_chars[0, column.limit] : value.to_s
else
comparison_operator = "= ?"
end
diff --git a/activerecord/lib/active_record/validator.rb b/activerecord/lib/active_record/validator.rb
new file mode 100644
index 0000000000..83a33f4dcd
--- /dev/null
+++ b/activerecord/lib/active_record/validator.rb
@@ -0,0 +1,68 @@
+module ActiveRecord #:nodoc:
+
+ # A simple base class that can be used along with ActiveRecord::Base.validates_with
+ #
+ # class Person < ActiveRecord::Base
+ # validates_with MyValidator
+ # end
+ #
+ # class MyValidator < ActiveRecord::Validator
+ # def validate
+ # if some_complex_logic
+ # record.errors[:base] = "This record is invalid"
+ # end
+ # end
+ #
+ # private
+ # def some_complex_logic
+ # # ...
+ # end
+ # end
+ #
+ # Any class that inherits from ActiveRecord::Validator will have access to <tt>record</tt>,
+ # which is an instance of the record being validated, and must implement a method called <tt>validate</tt>.
+ #
+ # class Person < ActiveRecord::Base
+ # validates_with MyValidator
+ # end
+ #
+ # class MyValidator < ActiveRecord::Validator
+ # def validate
+ # record # => The person instance being validated
+ # options # => Any non-standard options passed to validates_with
+ # end
+ # end
+ #
+ # To cause a validation error, you must add to the <tt>record<tt>'s errors directly
+ # from within the validators message
+ #
+ # class MyValidator < ActiveRecord::Validator
+ # def validate
+ # record.errors[:base] << "This is some custom error message"
+ # record.errors[:first_name] << "This is some complex validation"
+ # # etc...
+ # end
+ # end
+ #
+ # To add behavior to the initialize method, use the following signature:
+ #
+ # class MyValidator < ActiveRecord::Validator
+ # def initialize(record, options)
+ # super
+ # @my_custom_field = options[:field_name] || :first_name
+ # end
+ # end
+ #
+ class Validator
+ attr_reader :record, :options
+
+ def initialize(record, options)
+ @record = record
+ @options = options
+ end
+
+ def validate
+ raise "You must override this method"
+ end
+ end
+end
diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb
index 88136597e3..9463b7b14a 100644
--- a/activerecord/test/cases/adapter_test.rb
+++ b/activerecord/test/cases/adapter_test.rb
@@ -63,6 +63,18 @@ class AdapterTest < ActiveRecord::TestCase
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
+ assert_nothing_raised do
+ ActiveRecord::Base.establish_connection({
+ :adapter => 'mysql',
+ :username => 'rails'
+ })
+ ActiveRecord::Base.connection.execute "SELECT activerecord_unittest.pirates.*, activerecord_unittest2.courses.* FROM activerecord_unittest.pirates, activerecord_unittest2.courses"
+ end
+
+ ActiveRecord::Base.establish_connection 'arunit'
+ end
end
if current_adapter?(:PostgreSQLAdapter)
diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb
index 784c484178..2a77eed1b5 100644
--- a/activerecord/test/cases/associations/belongs_to_associations_test.rb
+++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb
@@ -249,24 +249,6 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_equal 1, Topic.find(topic.id)[:replies_count]
end
- def test_belongs_to_counter_after_save
- topic = Topic.create("title" => "monday night")
- topic.replies.create("title" => "re: monday night", "content" => "football")
- assert_equal 1, Topic.find(topic.id).send(:read_attribute, "replies_count")
-
- topic.save
- assert_equal 1, Topic.find(topic.id).send(:read_attribute, "replies_count")
- end
-
- def test_belongs_to_counter_after_update_attributes
- topic = Topic.create("title" => "37s")
- topic.replies.create("title" => "re: 37s", "content" => "rails")
- assert_equal 1, Topic.find(topic.id).send(:read_attribute, "replies_count")
-
- topic.update_attributes("title" => "37signals")
- assert_equal 1, Topic.find(topic.id).send(:read_attribute, "replies_count")
- end
-
def test_assignment_before_child_saved
final_cut = Client.new("name" => "Final Cut")
firm = Firm.find(1)
diff --git a/activerecord/test/cases/associations/habtm_join_table_test.rb b/activerecord/test/cases/associations/habtm_join_table_test.rb
new file mode 100644
index 0000000000..bf3e04c3eb
--- /dev/null
+++ b/activerecord/test/cases/associations/habtm_join_table_test.rb
@@ -0,0 +1,56 @@
+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
diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb
index a3d92c3bdb..f7178f2c5e 100644
--- a/activerecord/test/cases/associations/has_many_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_associations_test.rb
@@ -10,11 +10,12 @@ require 'models/author'
require 'models/comment'
require 'models/person'
require 'models/reader'
+require 'models/tagging'
class HasManyAssociationsTest < ActiveRecord::TestCase
fixtures :accounts, :categories, :companies, :developers, :projects,
:developers_projects, :topics, :authors, :comments, :author_addresses,
- :people, :posts, :readers
+ :people, :posts, :readers, :taggings
def setup
Client.destroyed_client_ids.clear
@@ -287,6 +288,12 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal client2, firm.clients.find(:first, :conditions => ["#{QUOTED_TYPE} = :type", { :type => 'Client' }], :order => "id")
end
+ def test_find_all_with_include_and_conditions
+ assert_nothing_raised do
+ Developer.find(:all, :joins => :audit_logs, :conditions => {'audit_logs.message' => nil, :name => 'Smith'})
+ end
+ end
+
def test_find_in_collection
assert_equal Client.find(2).name, companies(:first_firm).clients.find(2).name
assert_raise(ActiveRecord::RecordNotFound) { companies(:first_firm).clients.find(6) }
@@ -510,6 +517,23 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal 0, new_firm.clients_of_firm.size
end
+ def test_deleting_updates_counter_cache
+ topic = Topic.first
+ assert_equal topic.replies.to_a.size, topic.replies_count
+
+ topic.replies.delete(topic.replies.first)
+ topic.reload
+ assert_equal topic.replies.to_a.size, topic.replies_count
+ end
+
+ def test_deleting_updates_counter_cache_without_dependent_destroy
+ post = posts(:welcome)
+
+ assert_difference "post.reload.taggings_count", -1 do
+ post.taggings.delete(post.taggings.first)
+ end
+ end
+
def test_deleting_a_collection
force_signal37_to_load_all_clients_of_firm
companies(:first_firm).clients_of_firm.create("name" => "Another Client")
@@ -555,6 +579,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
end
+ def test_clearing_updates_counter_cache
+ topic = Topic.first
+
+ topic.replies.clear
+ topic.reload
+ assert_equal 0, topic.replies_count
+ end
+
def test_clearing_a_dependent_association_collection
firm = companies(:first_firm)
client_id = firm.dependent_clients_of_firm.first.id
@@ -699,6 +731,28 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal 0, companies(:first_firm).clients_of_firm(true).size
end
+ def test_destroying_by_fixnum_id
+ force_signal37_to_load_all_clients_of_firm
+
+ assert_difference "Client.count", -1 do
+ companies(:first_firm).clients_of_firm.destroy(companies(:first_firm).clients_of_firm.first.id)
+ end
+
+ assert_equal 0, companies(:first_firm).reload.clients_of_firm.size
+ assert_equal 0, companies(:first_firm).clients_of_firm(true).size
+ end
+
+ def test_destroying_by_string_id
+ force_signal37_to_load_all_clients_of_firm
+
+ assert_difference "Client.count", -1 do
+ companies(:first_firm).clients_of_firm.destroy(companies(:first_firm).clients_of_firm.first.id.to_s)
+ end
+
+ assert_equal 0, companies(:first_firm).reload.clients_of_firm.size
+ assert_equal 0, companies(:first_firm).clients_of_firm(true).size
+ end
+
def test_destroying_a_collection
force_signal37_to_load_all_clients_of_firm
companies(:first_firm).clients_of_firm.create("name" => "Another Client")
@@ -870,7 +924,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
lambda { authors(:mary).comments = [comments(:greetings), comments(:more_greetings)] },
lambda { authors(:mary).comments << Comment.create!(:body => "Yay", :post_id => 424242) },
lambda { authors(:mary).comments.delete(authors(:mary).comments.first) },
- ].each {|block| assert_raise(ActiveRecord::HasManyThroughCantAssociateThroughHasManyReflection, &block) }
+ ].each {|block| assert_raise(ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection, &block) }
end
def test_dynamic_find_should_respect_association_order_for_through
diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb
index 799ab52025..5f13b66d11 100644
--- a/activerecord/test/cases/associations/has_many_through_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb
@@ -11,9 +11,12 @@ 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, :jobs, :references
+ fixtures :posts, :readers, :people, :comments, :authors, :owners, :pets, :toys, :jobs, :references, :companies
def test_associate_existing
assert_queries(2) { posts(:thinking);people(:david) }
@@ -176,6 +179,44 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
assert_raises(ActiveRecord::RecordNotSaved) { p.people.create!(:first_name => "snow") }
end
+ def test_associate_with_create_and_invalid_options
+ firm = companies(:first_firm)
+ assert_no_difference('firm.developers.count') { assert_nothing_raised { firm.developers.create(:name => '0') } }
+ end
+
+ def test_associate_with_create_and_valid_options
+ firm = companies(:first_firm)
+ assert_difference('firm.developers.count', 1) { firm.developers.create(:name => 'developer') }
+ end
+
+ def test_associate_with_create_bang_and_invalid_options
+ firm = companies(:first_firm)
+ assert_no_difference('firm.developers.count') { assert_raises(ActiveRecord::RecordInvalid) { firm.developers.create!(:name => '0') } }
+ end
+
+ def test_associate_with_create_bang_and_valid_options
+ firm = companies(:first_firm)
+ assert_difference('firm.developers.count', 1) { firm.developers.create!(:name => 'developer') }
+ end
+
+ def test_push_with_invalid_record
+ firm = companies(:first_firm)
+ assert_raises(ActiveRecord::RecordInvalid) { firm.developers << Developer.new(:name => '0') }
+ end
+
+ def test_push_with_invalid_join_record
+ repair_validations(Contract) do
+ Contract.validate {|r| r.errors[:base] << 'Invalid Contract' }
+
+ firm = companies(:first_firm)
+ lifo = Developer.new(:name => 'lifo')
+ assert_raises(ActiveRecord::RecordInvalid) { firm.developers << lifo }
+
+ lifo = Developer.create!(:name => 'lifo')
+ assert_raises(ActiveRecord::RecordInvalid) { firm.developers << lifo }
+ end
+ end
+
def test_clear_associations
assert_queries(2) { posts(:welcome);posts(:welcome).people(true) }
@@ -304,4 +345,16 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
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
diff --git a/activerecord/test/cases/associations/has_one_through_associations_test.rb b/activerecord/test/cases/associations/has_one_through_associations_test.rb
index ab6e6d20fc..9aef3eb374 100644
--- a/activerecord/test/cases/associations/has_one_through_associations_test.rb
+++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb
@@ -28,6 +28,16 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
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")
diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb
index 9da7fc2639..e9af5a60d8 100644
--- a/activerecord/test/cases/associations/join_model_test.rb
+++ b/activerecord/test/cases/associations/join_model_test.rb
@@ -319,11 +319,11 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
end
def test_belongs_to_polymorphic_with_counter_cache
- assert_equal 0, posts(:welcome)[:taggings_count]
+ assert_equal 1, posts(:welcome)[:taggings_count]
tagging = posts(:welcome).taggings.create(:tag => tags(:general))
- assert_equal 1, posts(:welcome, :reload)[:taggings_count]
+ assert_equal 2, posts(:welcome, :reload)[:taggings_count]
tagging.destroy
- assert posts(:welcome, :reload)[:taggings_count].zero?
+ assert_equal 1, posts(:welcome, :reload)[:taggings_count]
end
def test_unavailable_through_reflection
@@ -381,7 +381,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
end
def test_has_many_through_polymorphic_has_one
- assert_raise(ActiveRecord::HasManyThroughSourceAssociationMacroError) { authors(:david).tagging }
+ assert_equal Tagging.find(1,2).sort_by { |t| t.id }, authors(:david).tagging
end
def test_has_many_through_polymorphic_has_many
diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb
index 16364141df..8421a8fb07 100755
--- a/activerecord/test/cases/base_test.rb
+++ b/activerecord/test/cases/base_test.rb
@@ -1070,7 +1070,25 @@ class BasicsTest < ActiveRecord::TestCase
assert_date_from_db Date.new(2004, 6, 24), topic.last_read.to_date
end
- def test_multiparameter_attributes_on_date_with_empty_date
+ def test_multiparameter_attributes_on_date_with_empty_year
+ attributes = { "last_read(1i)" => "", "last_read(2i)" => "6", "last_read(3i)" => "24" }
+ topic = Topic.find(1)
+ topic.attributes = attributes
+ # note that extra #to_date call allows test to pass for Oracle, which
+ # treats dates/times the same
+ assert_date_from_db Date.new(1, 6, 24), topic.last_read.to_date
+ end
+
+ def test_multiparameter_attributes_on_date_with_empty_month
+ attributes = { "last_read(1i)" => "2004", "last_read(2i)" => "", "last_read(3i)" => "24" }
+ topic = Topic.find(1)
+ topic.attributes = attributes
+ # note that extra #to_date call allows test to pass for Oracle, which
+ # treats dates/times the same
+ assert_date_from_db Date.new(2004, 1, 24), topic.last_read.to_date
+ end
+
+ def test_multiparameter_attributes_on_date_with_empty_day
attributes = { "last_read(1i)" => "2004", "last_read(2i)" => "6", "last_read(3i)" => "" }
topic = Topic.find(1)
topic.attributes = attributes
@@ -1079,6 +1097,33 @@ class BasicsTest < ActiveRecord::TestCase
assert_date_from_db Date.new(2004, 6, 1), topic.last_read.to_date
end
+ def test_multiparameter_attributes_on_date_with_empty_day_and_year
+ attributes = { "last_read(1i)" => "", "last_read(2i)" => "6", "last_read(3i)" => "" }
+ topic = Topic.find(1)
+ topic.attributes = attributes
+ # note that extra #to_date call allows test to pass for Oracle, which
+ # treats dates/times the same
+ assert_date_from_db Date.new(1, 6, 1), topic.last_read.to_date
+ end
+
+ def test_multiparameter_attributes_on_date_with_empty_day_and_month
+ attributes = { "last_read(1i)" => "2004", "last_read(2i)" => "", "last_read(3i)" => "" }
+ topic = Topic.find(1)
+ topic.attributes = attributes
+ # note that extra #to_date call allows test to pass for Oracle, which
+ # treats dates/times the same
+ assert_date_from_db Date.new(2004, 1, 1), topic.last_read.to_date
+ end
+
+ def test_multiparameter_attributes_on_date_with_empty_year_and_month
+ attributes = { "last_read(1i)" => "", "last_read(2i)" => "", "last_read(3i)" => "24" }
+ topic = Topic.find(1)
+ topic.attributes = attributes
+ # note that extra #to_date call allows test to pass for Oracle, which
+ # treats dates/times the same
+ assert_date_from_db Date.new(1, 1, 24), topic.last_read.to_date
+ end
+
def test_multiparameter_attributes_on_date_with_all_empty
attributes = { "last_read(1i)" => "", "last_read(2i)" => "", "last_read(3i)" => "" }
topic = Topic.find(1)
@@ -1730,17 +1775,15 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal res4, res5
- unless current_adapter?(:SQLite2Adapter, :DeprecatedSQLiteAdapter)
- res6 = Post.count_by_sql "SELECT COUNT(DISTINCT p.id) FROM posts p, comments co WHERE p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id"
- res7 = nil
- assert_nothing_raised do
- res7 = Post.count(:conditions => "p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id",
- :joins => "p, comments co",
- :select => "p.id",
- :distinct => true)
- end
- assert_equal res6, res7
+ res6 = Post.count_by_sql "SELECT COUNT(DISTINCT p.id) FROM posts p, comments co WHERE p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id"
+ res7 = nil
+ assert_nothing_raised do
+ res7 = Post.count(:conditions => "p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id",
+ :joins => "p, comments co",
+ :select => "p.id",
+ :distinct => true)
end
+ assert_equal res6, res7
end
def test_clear_association_cache_stored
diff --git a/activerecord/test/cases/column_definition_test.rb b/activerecord/test/cases/column_definition_test.rb
index 98abc8eac8..fc9a0ac96e 100644
--- a/activerecord/test/cases/column_definition_test.rb
+++ b/activerecord/test/cases/column_definition_test.rb
@@ -33,4 +33,38 @@ class ColumnDefinitionTest < ActiveRecord::TestCase
column.limit, column.precision, column.scale, column.default, column.null)
assert_equal %Q{title varchar(20) DEFAULT 'Hello' NOT NULL}, column_def.to_sql
end
+
+ if current_adapter?(:MysqlAdapter)
+ def test_should_set_default_for_mysql_binary_data_types
+ binary_column = ActiveRecord::ConnectionAdapters::MysqlColumn.new("title", "a", "binary(1)")
+ assert_equal "a", binary_column.default
+
+ varbinary_column = ActiveRecord::ConnectionAdapters::MysqlColumn.new("title", "a", "varbinary(1)")
+ assert_equal "a", varbinary_column.default
+ end
+
+ def test_should_not_set_default_for_blob_and_text_data_types
+ assert_raise ArgumentError do
+ ActiveRecord::ConnectionAdapters::MysqlColumn.new("title", "a", "blob")
+ end
+
+ assert_raise ArgumentError do
+ ActiveRecord::ConnectionAdapters::MysqlColumn.new("title", "Hello", "text")
+ end
+
+ text_column = ActiveRecord::ConnectionAdapters::MysqlColumn.new("title", nil, "text")
+ assert_equal nil, text_column.default
+
+ not_null_text_column = ActiveRecord::ConnectionAdapters::MysqlColumn.new("title", nil, "text", false)
+ assert_equal "", not_null_text_column.default
+ end
+
+ def test_has_default_should_return_false_for_blog_and_test_data_types
+ blob_column = ActiveRecord::ConnectionAdapters::MysqlColumn.new("title", nil, "blob")
+ assert !blob_column.has_default?
+
+ text_column = ActiveRecord::ConnectionAdapters::MysqlColumn.new("title", nil, "text")
+ assert !text_column.has_default?
+ end
+ end
end
diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb
index 74571d923a..f456d273fe 100644
--- a/activerecord/test/cases/dirty_test.rb
+++ b/activerecord/test/cases/dirty_test.rb
@@ -308,6 +308,84 @@ class DirtyTest < ActiveRecord::TestCase
end
end
+ def test_previous_changes
+ # original values should be in previous_changes
+ pirate = Pirate.new
+
+ assert_equal Hash.new, pirate.previous_changes
+ pirate.catchphrase = "arrr"
+ pirate.save!
+
+ assert_equal 4, pirate.previous_changes.size
+ assert_equal [nil, "arrr"], pirate.previous_changes['catchphrase']
+ assert_equal [nil, pirate.id], pirate.previous_changes['id']
+ assert_nil pirate.previous_changes['updated_on'][0]
+ assert_not_nil pirate.previous_changes['updated_on'][1]
+ assert_nil pirate.previous_changes['created_on'][0]
+ assert_not_nil pirate.previous_changes['created_on'][1]
+ assert !pirate.previous_changes.key?('parrot_id')
+
+ # original values should be in previous_changes
+ pirate = Pirate.new
+
+ assert_equal Hash.new, pirate.previous_changes
+ pirate.catchphrase = "arrr"
+ pirate.save
+
+ assert_equal 4, pirate.previous_changes.size
+ assert_equal [nil, "arrr"], pirate.previous_changes['catchphrase']
+ assert_equal [nil, pirate.id], pirate.previous_changes['id']
+ assert pirate.previous_changes.include?('updated_on')
+ assert pirate.previous_changes.include?('created_on')
+ assert !pirate.previous_changes.key?('parrot_id')
+
+ pirate.catchphrase = "Yar!!"
+ pirate.reload
+ assert_equal Hash.new, pirate.previous_changes
+
+ pirate = Pirate.find_by_catchphrase("arrr")
+ pirate.catchphrase = "Me Maties!"
+ pirate.save!
+
+ assert_equal 2, pirate.previous_changes.size
+ assert_equal ["arrr", "Me Maties!"], pirate.previous_changes['catchphrase']
+ assert_not_nil pirate.previous_changes['updated_on'][0]
+ assert_not_nil pirate.previous_changes['updated_on'][1]
+ assert !pirate.previous_changes.key?('parrot_id')
+ assert !pirate.previous_changes.key?('created_on')
+
+ pirate = Pirate.find_by_catchphrase("Me Maties!")
+ pirate.catchphrase = "Thar She Blows!"
+ pirate.save
+
+ assert_equal 2, pirate.previous_changes.size
+ assert_equal ["Me Maties!", "Thar She Blows!"], pirate.previous_changes['catchphrase']
+ assert_not_nil pirate.previous_changes['updated_on'][0]
+ assert_not_nil pirate.previous_changes['updated_on'][1]
+ assert !pirate.previous_changes.key?('parrot_id')
+ assert !pirate.previous_changes.key?('created_on')
+
+ pirate = Pirate.find_by_catchphrase("Thar She Blows!")
+ pirate.update_attributes(:catchphrase => "Ahoy!")
+
+ assert_equal 2, pirate.previous_changes.size
+ assert_equal ["Thar She Blows!", "Ahoy!"], pirate.previous_changes['catchphrase']
+ assert_not_nil pirate.previous_changes['updated_on'][0]
+ assert_not_nil pirate.previous_changes['updated_on'][1]
+ assert !pirate.previous_changes.key?('parrot_id')
+ assert !pirate.previous_changes.key?('created_on')
+
+ pirate = Pirate.find_by_catchphrase("Ahoy!")
+ pirate.update_attribute(:catchphrase, "Ninjas suck!")
+
+ assert_equal 2, pirate.previous_changes.size
+ assert_equal ["Ahoy!", "Ninjas suck!"], pirate.previous_changes['catchphrase']
+ assert_not_nil pirate.previous_changes['updated_on'][0]
+ assert_not_nil pirate.previous_changes['updated_on'][1]
+ assert !pirate.previous_changes.key?('parrot_id')
+ assert !pirate.previous_changes.key?('created_on')
+ end
+
private
def with_partial_updates(klass, on = true)
old = klass.partial_updates?
diff --git a/activerecord/test/cases/i18n_test.rb b/activerecord/test/cases/i18n_test.rb
index b1db662eca..d59c53cec8 100644
--- a/activerecord/test/cases/i18n_test.rb
+++ b/activerecord/test/cases/i18n_test.rb
@@ -12,6 +12,11 @@ class ActiveRecordI18nTests < Test::Unit::TestCase
I18n.backend.store_translations 'en', :activerecord => {:attributes => {:topic => {:title => 'topic title attribute'} } }
assert_equal 'topic title attribute', Topic.human_attribute_name('title')
end
+
+ def test_translated_model_attributes_with_symbols
+ I18n.backend.store_translations 'en', :activerecord => {:attributes => {:topic => {:title => 'topic title attribute'} } }
+ assert_equal 'topic title attribute', Topic.human_attribute_name(:title)
+ end
def test_translated_model_attributes_with_sti
I18n.backend.store_translations 'en', :activerecord => {:attributes => {:reply => {:title => 'reply title attribute'} } }
diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb
index f0f21615e0..6d3f938799 100644
--- a/activerecord/test/cases/migration_test.rb
+++ b/activerecord/test/cases/migration_test.rb
@@ -25,6 +25,24 @@ if ActiveRecord::Base.connection.supports_migrations?
end
end
+ class MigrationTableAndIndexTest < ActiveRecord::TestCase
+ def test_add_schema_info_respects_prefix_and_suffix
+ conn = ActiveRecord::Base.connection
+
+ conn.drop_table(ActiveRecord::Migrator.schema_migrations_table_name) if conn.table_exists?(ActiveRecord::Migrator.schema_migrations_table_name)
+ ActiveRecord::Base.table_name_prefix = 'foo_'
+ ActiveRecord::Base.table_name_suffix = '_bar'
+ conn.drop_table(ActiveRecord::Migrator.schema_migrations_table_name) if conn.table_exists?(ActiveRecord::Migrator.schema_migrations_table_name)
+
+ conn.initialize_schema_migrations_table
+
+ assert_equal "foo_unique_schema_migrations_bar", conn.indexes(ActiveRecord::Migrator.schema_migrations_table_name)[0][:name]
+ ensure
+ ActiveRecord::Base.table_name_prefix = ""
+ ActiveRecord::Base.table_name_suffix = ""
+ end
+ end
+
class MigrationTest < ActiveRecord::TestCase
self.use_transactional_fixtures = false
@@ -224,7 +242,7 @@ if ActiveRecord::Base.connection.supports_migrations?
t.column :foo, :string
end
- assert_equal %w(foo testings_id), Person.connection.columns(:testings).map { |c| c.name }.sort
+ assert_equal %w(foo testing_id), Person.connection.columns(:testings).map { |c| c.name }.sort
ensure
Person.connection.drop_table :testings rescue nil
ActiveRecord::Base.primary_key_prefix_type = nil
@@ -237,7 +255,7 @@ if ActiveRecord::Base.connection.supports_migrations?
t.column :foo, :string
end
- assert_equal %w(foo testingsid), Person.connection.columns(:testings).map { |c| c.name }.sort
+ assert_equal %w(foo testingid), Person.connection.columns(:testings).map { |c| c.name }.sort
ensure
Person.connection.drop_table :testings rescue nil
ActiveRecord::Base.primary_key_prefix_type = nil
@@ -396,7 +414,7 @@ if ActiveRecord::Base.connection.supports_migrations?
assert_equal 9, wealth_column.precision
assert_equal 7, wealth_column.scale
end
-
+
def test_native_types
Person.delete_all
Person.connection.add_column "people", "last_name", :string
@@ -975,9 +993,9 @@ if ActiveRecord::Base.connection.supports_migrations?
def test_migrator_one_down
ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/valid")
-
+
ActiveRecord::Migrator.down(MIGRATIONS_ROOT + "/valid", 1)
-
+
Person.reset_column_information
assert Person.column_methods_hash.include?(:last_name)
assert !Reminder.table_exists?
@@ -1118,20 +1136,20 @@ if ActiveRecord::Base.connection.supports_migrations?
assert Reminder.create("content" => "hello world", "remind_at" => Time.now)
assert_equal "hello world", Reminder.find(:first).content
end
-
+
def test_migrator_rollback
ActiveRecord::Migrator.migrate(MIGRATIONS_ROOT + "/valid")
assert_equal(3, ActiveRecord::Migrator.current_version)
-
+
ActiveRecord::Migrator.rollback(MIGRATIONS_ROOT + "/valid")
assert_equal(2, ActiveRecord::Migrator.current_version)
-
+
ActiveRecord::Migrator.rollback(MIGRATIONS_ROOT + "/valid")
assert_equal(1, ActiveRecord::Migrator.current_version)
-
+
ActiveRecord::Migrator.rollback(MIGRATIONS_ROOT + "/valid")
assert_equal(0, ActiveRecord::Migrator.current_version)
-
+
ActiveRecord::Migrator.rollback(MIGRATIONS_ROOT + "/valid")
assert_equal(0, ActiveRecord::Migrator.current_version)
end
@@ -1294,7 +1312,7 @@ if ActiveRecord::Base.connection.supports_migrations?
end
end
-
+
class SexyMigrationsTest < ActiveRecord::TestCase
def test_references_column_type_adds_id
with_new_table do |t|
@@ -1350,6 +1368,15 @@ if ActiveRecord::Base.connection.supports_migrations?
end
end
+ if current_adapter?(:PostgreSQLAdapter)
+ def test_xml_creates_xml_column
+ with_new_table do |t|
+ t.expects(:column).with(:data, 'xml', {})
+ t.xml :data
+ end
+ end
+ end
+
protected
def with_new_table
Person.connection.create_table :delete_me, :force => true do |t|
@@ -1567,3 +1594,4 @@ if ActiveRecord::Base.connection.supports_migrations?
end
end
end
+
diff --git a/activerecord/test/cases/named_scope_test.rb b/activerecord/test/cases/named_scope_test.rb
index 2a729f0678..bb2aba9d92 100644
--- a/activerecord/test/cases/named_scope_test.rb
+++ b/activerecord/test/cases/named_scope_test.rb
@@ -368,6 +368,12 @@ class NamedScopeTest < ActiveRecord::TestCase
end
end
end
+
+ def test_table_names_for_chaining_scopes_with_and_without_table_name_included
+ assert_nothing_raised do
+ Comment.for_first_post.for_first_author.all
+ end
+ end
end
class DynamicScopeMatchTest < ActiveRecord::TestCase
diff --git a/activerecord/test/cases/pk_test.rb b/activerecord/test/cases/pk_test.rb
index 948a570b93..c121e0aa0f 100644
--- a/activerecord/test/cases/pk_test.rb
+++ b/activerecord/test/cases/pk_test.rb
@@ -98,4 +98,22 @@ class PrimaryKeysTest < ActiveRecord::TestCase
def test_instance_destroy_should_quote_pkey
assert_nothing_raised { MixedCaseMonkey.find(1).destroy }
end
+
+ def test_supports_primary_key
+ assert_nothing_raised NoMethodError do
+ ActiveRecord::Base.connection.supports_primary_key?
+ end
+ end
+
+ def test_primary_key_returns_value_if_it_exists
+ if ActiveRecord::Base.connection.supports_primary_key?
+ assert_equal 'id', ActiveRecord::Base.connection.primary_key('developers')
+ end
+ end
+
+ def test_primary_key_returns_nil_if_it_does_not_exist
+ if ActiveRecord::Base.connection.supports_primary_key?
+ assert_nil ActiveRecord::Base.connection.primary_key('developers_projects')
+ end
+ end
end
diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb
index 4083b990d9..a164f5e060 100644
--- a/activerecord/test/cases/reflection_test.rb
+++ b/activerecord/test/cases/reflection_test.rb
@@ -176,8 +176,8 @@ class ReflectionTest < ActiveRecord::TestCase
def test_reflection_of_all_associations
# FIXME these assertions bust a lot
- assert_equal 29, Firm.reflect_on_all_associations.size
- assert_equal 22, Firm.reflect_on_all_associations(:has_many).size
+ assert_equal 31, Firm.reflect_on_all_associations.size
+ assert_equal 24, Firm.reflect_on_all_associations(:has_many).size
assert_equal 7, Firm.reflect_on_all_associations(:has_one).size
assert_equal 0, Firm.reflect_on_all_associations(:belongs_to).size
end
diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb
index e6a77f626b..1c43e3c5b5 100644
--- a/activerecord/test/cases/schema_dumper_test.rb
+++ b/activerecord/test/cases/schema_dumper_test.rb
@@ -161,7 +161,7 @@ class SchemaDumperTest < ActiveRecord::TestCase
index_definition = standard_dump.split(/\n/).grep(/add_index.*companies/).first.strip
assert_equal 'add_index "companies", ["firm_id", "type", "rating", "ruby_type"], :name => "company_index"', index_definition
end
-
+
def test_schema_dump_should_honor_nonstandard_primary_keys
output = standard_dump
match = output.match(%r{create_table "movies"(.*)do})
@@ -196,6 +196,15 @@ class SchemaDumperTest < ActiveRecord::TestCase
assert_match %r{:precision => 3,[[:space:]]+:scale => 2,[[:space:]]+:default => 2.78}, output
end
+ if current_adapter?(:PostgreSQLAdapter)
+ def test_schema_dump_includes_xml_shorthand_definition
+ output = standard_dump
+ if %r{create_table "postgresql_xml_data_type"} =~ output
+ assert_match %r{t.xml "data"}, output
+ end
+ end
+ end
+
def test_schema_dump_keeps_large_precision_integer_columns_as_decimal
output = standard_dump
# Oracle supports precision up to 38 and it identifies decimals with scale 0 as integers
@@ -214,3 +223,4 @@ class SchemaDumperTest < ActiveRecord::TestCase
assert_match %r{t.string[[:space:]]+"id",[[:space:]]+:null => false$}, match[2], "non-primary key id column not preserved"
end
end
+
diff --git a/activerecord/test/cases/validations/i18n_validation_test.rb b/activerecord/test/cases/validations/i18n_validation_test.rb
index 0303147e50..73d9c7249c 100644
--- a/activerecord/test/cases/validations/i18n_validation_test.rb
+++ b/activerecord/test/cases/validations/i18n_validation_test.rb
@@ -709,3 +709,191 @@ class I18nValidationTest < ActiveRecord::TestCase
assert_equal ["I am a custom error"], @topic.errors[:title]
end
end
+
+class ActiveRecordValidationsGenerateMessageI18nTests < ActiveSupport::TestCase
+ def setup
+ reset_callbacks Topic
+ @topic = Topic.new
+ I18n.backend.store_translations :'en', {
+ :activerecord => {
+ :errors => {
+ :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"
+ }
+ }
+ }
+ }
+ end
+
+ def reset_callbacks(*models)
+ models.each do |model|
+ model.instance_variable_set("@validate_callbacks", ActiveSupport::Callbacks::CallbackChain.new)
+ model.instance_variable_set("@validate_on_create_callbacks", ActiveSupport::Callbacks::CallbackChain.new)
+ model.instance_variable_set("@validate_on_update_callbacks", ActiveSupport::Callbacks::CallbackChain.new)
+ end
+ end
+
+ # validates_inclusion_of: generate_message(attr_name, :inclusion, :default => configuration[:message], :value => value)
+ def test_generate_message_inclusion_with_default_message
+ assert_equal 'is not included in the list', @topic.errors.generate_message(:title, :inclusion, :default => nil, :value => 'title')
+ end
+
+ def test_generate_message_inclusion_with_custom_message
+ assert_equal 'custom message title', @topic.errors.generate_message(:title, :inclusion, :default => 'custom message {{value}}', :value => 'title')
+ end
+
+ # validates_exclusion_of: generate_message(attr_name, :exclusion, :default => configuration[:message], :value => value)
+ def test_generate_message_exclusion_with_default_message
+ assert_equal 'is reserved', @topic.errors.generate_message(:title, :exclusion, :default => nil, :value => 'title')
+ end
+
+ def test_generate_message_exclusion_with_custom_message
+ assert_equal 'custom message title', @topic.errors.generate_message(:title, :exclusion, :default => 'custom message {{value}}', :value => 'title')
+ end
+
+ # validates_associated: generate_message(attr_name, :invalid, :default => configuration[:message], :value => value)
+ # validates_format_of: generate_message(attr_name, :invalid, :default => configuration[:message], :value => value)
+ def test_generate_message_invalid_with_default_message
+ assert_equal 'is invalid', @topic.errors.generate_message(:title, :invalid, :default => nil, :value => 'title')
+ end
+
+ def test_generate_message_invalid_with_custom_message
+ assert_equal 'custom message title', @topic.errors.generate_message(:title, :invalid, :default => 'custom message {{value}}', :value => 'title')
+ end
+
+ # validates_confirmation_of: generate_message(attr_name, :confirmation, :default => configuration[:message])
+ def test_generate_message_confirmation_with_default_message
+ assert_equal "doesn't match confirmation", @topic.errors.generate_message(:title, :confirmation, :default => nil)
+ end
+
+ def test_generate_message_confirmation_with_custom_message
+ assert_equal 'custom message', @topic.errors.generate_message(:title, :confirmation, :default => 'custom message')
+ end
+
+ # validates_acceptance_of: generate_message(attr_name, :accepted, :default => configuration[:message])
+ def test_generate_message_accepted_with_default_message
+ assert_equal "must be accepted", @topic.errors.generate_message(:title, :accepted, :default => nil)
+ end
+
+ def test_generate_message_accepted_with_custom_message
+ assert_equal 'custom message', @topic.errors.generate_message(:title, :accepted, :default => 'custom message')
+ end
+
+ # add_on_empty: generate_message(attr, :empty, :default => custom_message)
+ def test_generate_message_empty_with_default_message
+ assert_equal "can't be empty", @topic.errors.generate_message(:title, :empty, :default => nil)
+ end
+
+ def test_generate_message_empty_with_custom_message
+ assert_equal 'custom message', @topic.errors.generate_message(:title, :empty, :default => 'custom message')
+ end
+
+ # add_on_blank: generate_message(attr, :blank, :default => custom_message)
+ def test_generate_message_blank_with_default_message
+ assert_equal "can't be blank", @topic.errors.generate_message(:title, :blank, :default => nil)
+ end
+
+ def test_generate_message_blank_with_custom_message
+ assert_equal 'custom message', @topic.errors.generate_message(:title, :blank, :default => 'custom message')
+ end
+
+ # validates_length_of: generate_message(attr, :too_long, :default => options[:too_long], :count => option_value.end)
+ def test_generate_message_too_long_with_default_message
+ assert_equal "is too long (maximum is 10 characters)", @topic.errors.generate_message(:title, :too_long, :default => nil, :count => 10)
+ end
+
+ def test_generate_message_too_long_with_custom_message
+ assert_equal 'custom message 10', @topic.errors.generate_message(:title, :too_long, :default => 'custom message {{count}}', :count => 10)
+ end
+
+ # validates_length_of: generate_message(attr, :too_short, :default => options[:too_short], :count => option_value.begin)
+ def test_generate_message_too_short_with_default_message
+ assert_equal "is too short (minimum is 10 characters)", @topic.errors.generate_message(:title, :too_short, :default => nil, :count => 10)
+ end
+
+ def test_generate_message_too_short_with_custom_message
+ assert_equal 'custom message 10', @topic.errors.generate_message(:title, :too_short, :default => 'custom message {{count}}', :count => 10)
+ end
+
+ # validates_length_of: generate_message(attr, key, :default => custom_message, :count => option_value)
+ def test_generate_message_wrong_length_with_default_message
+ assert_equal "is the wrong length (should be 10 characters)", @topic.errors.generate_message(:title, :wrong_length, :default => nil, :count => 10)
+ end
+
+ def test_generate_message_wrong_length_with_custom_message
+ assert_equal 'custom message 10', @topic.errors.generate_message(:title, :wrong_length, :default => 'custom message {{count}}', :count => 10)
+ end
+
+ # validates_uniqueness_of: generate_message(attr_name, :taken, :default => configuration[:message])
+ def test_generate_message_taken_with_default_message
+ assert_equal "has already been taken", @topic.errors.generate_message(:title, :taken, :default => nil, :value => 'title')
+ end
+
+ def test_generate_message_taken_with_custom_message
+ assert_equal 'custom message title', @topic.errors.generate_message(:title, :taken, :default => 'custom message {{value}}', :value => 'title')
+ end
+
+ # validates_numericality_of: generate_message(attr_name, :not_a_number, :value => raw_value, :default => configuration[:message])
+ def test_generate_message_not_a_number_with_default_message
+ assert_equal "is not a number", @topic.errors.generate_message(:title, :not_a_number, :default => nil, :value => 'title')
+ end
+
+ def test_generate_message_not_a_number_with_custom_message
+ assert_equal 'custom message title', @topic.errors.generate_message(:title, :not_a_number, :default => 'custom message {{value}}', :value => 'title')
+ end
+
+ # validates_numericality_of: generate_message(attr_name, option, :value => raw_value, :default => configuration[:message])
+ def test_generate_message_greater_than_with_default_message
+ assert_equal "must be greater than 10", @topic.errors.generate_message(:title, :greater_than, :default => nil, :value => 'title', :count => 10)
+ end
+
+ def test_generate_message_greater_than_or_equal_to_with_default_message
+ assert_equal "must be greater than or equal to 10", @topic.errors.generate_message(:title, :greater_than_or_equal_to, :default => nil, :value => 'title', :count => 10)
+ end
+
+ def test_generate_message_equal_to_with_default_message
+ assert_equal "must be equal to 10", @topic.errors.generate_message(:title, :equal_to, :default => nil, :value => 'title', :count => 10)
+ end
+
+ def test_generate_message_less_than_with_default_message
+ assert_equal "must be less than 10", @topic.errors.generate_message(:title, :less_than, :default => nil, :value => 'title', :count => 10)
+ end
+
+ def test_generate_message_less_than_or_equal_to_with_default_message
+ assert_equal "must be less than or equal to 10", @topic.errors.generate_message(:title, :less_than_or_equal_to, :default => nil, :value => 'title', :count => 10)
+ end
+
+ def test_generate_message_odd_with_default_message
+ assert_equal "must be odd", @topic.errors.generate_message(:title, :odd, :default => nil, :value => 'title', :count => 10)
+ end
+
+ def test_generate_message_even_with_default_message
+ assert_equal "must be even", @topic.errors.generate_message(:title, :even, :default => nil, :value => 'title', :count => 10)
+ end
+ # ActiveRecord#RecordInvalid exception
+
+ test "RecordInvalid exception can be localized" do
+ topic = Topic.new
+ topic.errors.add(:title, :invalid)
+ topic.errors.add(:title, :blank)
+ assert_equal "Validation failed: Title is invalid, Title can't be blank", ActiveRecord::RecordInvalid.new(topic).message
+ end
+end
diff --git a/activerecord/test/cases/validations/uniqueness_validation_test.rb b/activerecord/test/cases/validations/uniqueness_validation_test.rb
index 961db51d1d..17ba4e2f8a 100644
--- a/activerecord/test/cases/validations/uniqueness_validation_test.rb
+++ b/activerecord/test/cases/validations/uniqueness_validation_test.rb
@@ -5,6 +5,7 @@ require 'models/reply'
require 'models/warehouse_thing'
require 'models/guid'
require 'models/event'
+require 'models/developer'
# The following methods in Topic are used in test_conditional_validation_*
class Topic
@@ -36,20 +37,20 @@ class Thaumaturgist < IneptWizard
end
class UniquenessValidationTest < ActiveRecord::TestCase
- fixtures :topics, 'warehouse-things'
+ fixtures :topics, 'warehouse-things', :developers
repair_validations(Topic)
def test_validate_uniqueness
Topic.validates_uniqueness_of(:title)
- t = Topic.new("title" => "I'm unique!")
+ t = Topic.new("title" => "I'm uniqué!")
assert t.save, "Should save t as unique"
t.content = "Remaining unique"
assert t.save, "Should still save t as unique"
- t2 = Topic.new("title" => "I'm unique!")
+ t2 = Topic.new("title" => "I'm uniqué!")
assert !t2.valid?, "Shouldn't be valid"
assert !t2.save, "Shouldn't save t2 as unique"
assert_equal ["has already been taken"], t2.errors[:title]
@@ -58,7 +59,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase
assert t2.save, "Should now save t2 as unique"
end
- def test_validates_uniquness_with_newline_chars
+ def test_validates_uniqueness_with_newline_chars
Topic.validates_uniqueness_of(:title, :case_sensitive => false)
t = Topic.new("title" => "new\nline")
@@ -237,6 +238,16 @@ class UniquenessValidationTest < ActiveRecord::TestCase
assert !e2.valid?, "Created an event whose title, with limit taken into account, is not unique"
end
+ def test_validate_uniqueness_with_limit_and_utf8
+ with_kcode('UTF8') do
+ # Event.title is limited to 5 characters
+ e1 = Event.create(:title => "一二三四五")
+ assert e1.valid?, "Could not create an event with a unique, 5 character title"
+ e2 = Event.create(:title => "一二三四五六七八")
+ assert !e2.valid?, "Created an event whose title, with limit taken into account, is not unique"
+ end
+ end
+
def test_validate_straight_inheritance_uniqueness
w1 = IneptWizard.create(:name => "Rincewind", :city => "Ankh-Morpork")
assert w1.valid?, "Saving w1"
diff --git a/activerecord/test/connections/native_sqlite/connection.rb b/activerecord/test/connections/native_sqlite/connection.rb
deleted file mode 100644
index fea985d8a3..0000000000
--- a/activerecord/test/connections/native_sqlite/connection.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-print "Using native SQlite\n"
-require_dependency 'models/course'
-require 'logger'
-ActiveRecord::Base.logger = Logger.new("debug.log")
-
-class SqliteError < StandardError
-end
-
-BASE_DIR = FIXTURES_ROOT
-sqlite_test_db = "#{BASE_DIR}/fixture_database.sqlite"
-sqlite_test_db2 = "#{BASE_DIR}/fixture_database_2.sqlite"
-
-def make_connection(clazz, db_file)
- ActiveRecord::Base.configurations = { clazz.name => { :adapter => 'sqlite', :database => db_file } }
- unless File.exist?(db_file)
- puts "SQLite database not found at #{db_file}. Rebuilding it."
- sqlite_command = %Q{sqlite "#{db_file}" "create table a (a integer); drop table a;"}
- puts "Executing '#{sqlite_command}'"
- raise SqliteError.new("Seems that there is no sqlite executable available") unless system(sqlite_command)
- end
- clazz.establish_connection(clazz.name)
-end
-
-make_connection(ActiveRecord::Base, sqlite_test_db)
-make_connection(Course, sqlite_test_db2)
diff --git a/activerecord/test/fixtures/posts.yml b/activerecord/test/fixtures/posts.yml
index 92e5d1908f..f817493190 100644
--- a/activerecord/test/fixtures/posts.yml
+++ b/activerecord/test/fixtures/posts.yml
@@ -4,6 +4,7 @@ welcome:
title: Welcome to the weblog
body: Such a lovely day
comments_count: 2
+ taggings_count: 1
type: Post
thinking:
@@ -11,6 +12,8 @@ thinking:
author_id: 1
title: So I was thinking
body: Like I hopefully always am
+ comments_count: 1
+ taggings_count: 1
type: SpecialPost
authorless:
diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb
index b844c7cce0..f264f980d6 100644
--- a/activerecord/test/models/author.rb
+++ b/activerecord/test/models/author.rb
@@ -1,5 +1,6 @@
class Author < ActiveRecord::Base
has_many :posts
+ has_many :very_special_comments, :through => :posts
has_many :posts_with_comments, :include => :comments, :class_name => "Post"
has_many :popular_grouped_posts, :include => :comments, :class_name => "Post", :group => "type", :having => "SUM(comments_count) > 1", :select => "type"
has_many :posts_with_comments_sorted_by_comment_id, :include => :comments, :class_name => "Post", :order => 'comments.id'
diff --git a/activerecord/test/models/comment.rb b/activerecord/test/models/comment.rb
index f7f07c103f..399dea9f12 100644
--- a/activerecord/test/models/comment.rb
+++ b/activerecord/test/models/comment.rb
@@ -1,6 +1,10 @@
class Comment < ActiveRecord::Base
named_scope :containing_the_letter_e, :conditions => "comments.body LIKE '%e%'"
-
+ named_scope :for_first_post, :conditions => { :post_id => 1 }
+ named_scope :for_first_author,
+ :joins => :post,
+ :conditions => { "posts.author_id" => 1 }
+
belongs_to :post, :counter_cache => true
def self.what_are_you
diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb
index 1c05e523e0..ab09f88a9f 100644
--- a/activerecord/test/models/company.rb
+++ b/activerecord/test/models/company.rb
@@ -9,6 +9,8 @@ class Company < AbstractCompany
validates_presence_of :name
has_one :dummy_account, :foreign_key => "firm_id", :class_name => "Account"
+ has_many :contracts
+ has_many :developers, :through => :contracts
def arbitrary_method
"I am Jack's profound disappointment"
diff --git a/activerecord/test/models/contract.rb b/activerecord/test/models/contract.rb
new file mode 100644
index 0000000000..606c99cd4e
--- /dev/null
+++ b/activerecord/test/models/contract.rb
@@ -0,0 +1,4 @@
+class Contract < ActiveRecord::Base
+ belongs_to :company
+ belongs_to :developer
+end \ No newline at end of file
diff --git a/activerecord/test/schema/postgresql_specific_schema.rb b/activerecord/test/schema/postgresql_specific_schema.rb
index 576a4d03c6..3d8911bfe9 100644
--- a/activerecord/test/schema/postgresql_specific_schema.rb
+++ b/activerecord/test/schema/postgresql_specific_schema.rb
@@ -1,7 +1,7 @@
ActiveRecord::Schema.define do
%w(postgresql_arrays postgresql_moneys postgresql_numbers postgresql_times postgresql_network_addresses postgresql_bit_strings
- postgresql_oids defaults geometrics).each do |table_name|
+ postgresql_oids postgresql_xml_data_type defaults geometrics).each do |table_name|
execute "DROP TABLE IF EXISTS #{quote_table_name table_name}"
end
@@ -100,4 +100,15 @@ _SQL
obj_id OID
);
_SQL
-end \ No newline at end of file
+
+ begin
+ execute <<_SQL
+ CREATE TABLE postgresql_xml_data_type (
+ id SERIAL PRIMARY KEY,
+ data xml
+ );
+_SQL
+rescue #This version of PostgreSQL either has no XML support or is was not compiled with XML support: skipping table
+ end
+end
+
diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb
index 5f60d5e137..9ab4cf6f43 100644
--- a/activerecord/test/schema/schema.rb
+++ b/activerecord/test/schema/schema.rb
@@ -131,6 +131,10 @@ ActiveRecord::Schema.define do
t.integer :extendedWarranty, :null => false
end
+ create_table :contracts, :force => true do |t|
+ t.integer :developer_id
+ t.integer :company_id
+ end
create_table :customers, :force => true do |t|
t.string :name