diff options
Diffstat (limited to 'activerecord')
135 files changed, 2024 insertions, 1189 deletions
diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index b05d3970c7..4113a16c12 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -1,5 +1,17 @@ *Rails 3.1.0 (unreleased)* +* ActiveRecord::MacroReflection::AssociationReflection#build_record has a new method signature. + + Before: def build_association(*options) + After: def build_association(*options, &block) + + Users who are redefining this method to extend functionality should ensure that the block is + passed through to ActiveRecord::Base#new. + + This change is necessary to fix https://github.com/rails/rails/issues/1842. + + [Jon Leighton] + * AR#pluralize_table_names can be used to singularize/pluralize table name of an individual model: class User < ActiveRecord::Base @@ -240,7 +252,6 @@ def up create_table :posts do |t| t.belongs_to :user - t.timestamps end diff --git a/activerecord/README.rdoc b/activerecord/README.rdoc index 6b4c85bb93..822276589b 100644 --- a/activerecord/README.rdoc +++ b/activerecord/README.rdoc @@ -3,7 +3,7 @@ Active Record connects classes to relational database tables to establish an almost zero-configuration persistence layer for applications. The library provides a base class that, when subclassed, sets up a mapping between the new -class and an existing table in the database. In context of an application, +class and an existing table in the database. In the context of an application, these classes are commonly referred to as *models*. Models can also be connected to other models; this is done by defining *associations*. @@ -70,7 +70,7 @@ A short rundown of some of the major features: {Learn more}[link:classes/ActiveRecord/Validations.html] -* Callbacks available for the entire life cycle (instantiation, saving, destroying, validating, etc.) +* Callbacks available for the entire life cycle (instantiation, saving, destroying, validating, etc.). class Person < ActiveRecord::Base before_destroy :invalidate_payment_plan @@ -80,7 +80,7 @@ A short rundown of some of the major features: {Learn more}[link:classes/ActiveRecord/Callbacks.html] -* Observers that react to changes in a model +* Observers that react to changes in a model. class CommentObserver < ActiveRecord::Observer def after_create(comment) # is called just after Comment#save @@ -91,7 +91,7 @@ A short rundown of some of the major features: {Learn more}[link:classes/ActiveRecord/Observer.html] -* Inheritance hierarchies +* Inheritance hierarchies. class Company < ActiveRecord::Base; end class Firm < Company; end @@ -101,7 +101,7 @@ A short rundown of some of the major features: {Learn more}[link:classes/ActiveRecord/Base.html] -* Transactions +* Transactions. # Database transaction Account.transaction do @@ -112,7 +112,7 @@ A short rundown of some of the major features: {Learn more}[link:classes/ActiveRecord/Transactions/ClassMethods.html] -* Reflections on columns, associations, and aggregations +* Reflections on columns, associations, and aggregations. reflection = Firm.reflect_on_association(:clients) reflection.klass # => Client (class) @@ -121,7 +121,7 @@ A short rundown of some of the major features: {Learn more}[link:classes/ActiveRecord/Reflection/ClassMethods.html] -* Database abstraction through simple adapters +* Database abstraction through simple adapters. # connect to SQLite3 ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => "dbfile.sqlite3") @@ -141,13 +141,13 @@ A short rundown of some of the major features: SQLite3[link:classes/ActiveRecord/ConnectionAdapters/SQLite3Adapter.html]. -* Logging support for Log4r[http://log4r.sourceforge.net] and Logger[http://www.ruby-doc.org/stdlib/libdoc/logger/rdoc] +* Logging support for Log4r[http://log4r.sourceforge.net] and Logger[http://www.ruby-doc.org/stdlib/libdoc/logger/rdoc]. ActiveRecord::Base.logger = Logger.new(STDOUT) ActiveRecord::Base.logger = Log4r::Logger.new("Application Log") -* Database agnostic schema management with Migrations +* Database agnostic schema management with Migrations. class AddSystemSettings < ActiveRecord::Migration def self.up @@ -215,7 +215,7 @@ Active Record is released under the MIT license. API documentation is at -* http://api.rubyonrails.com +* http://api.rubyonrails.org Bug reports and feature requests can be filed with the rest for the Ruby on Rails project here: diff --git a/activerecord/RUNNING_UNIT_TESTS b/activerecord/RUNNING_UNIT_TESTS index b3d376772e..6a2e23b01f 100644 --- a/activerecord/RUNNING_UNIT_TESTS +++ b/activerecord/RUNNING_UNIT_TESTS @@ -1,43 +1,39 @@ -== Creating the test database +== Configure databases -The default names for the test databases are "activerecord_unittest" and -"activerecord_unittest2". If you want to use another database name, then be sure -to update the connection adapter setups you want to test within -test/connections/<your database>/connection.rb. -When you have the database online, you can import the fixture tables with -the test/schema/*.sql files. +Copy test/config.example.yml to test/config.yml and edit as needed. Or just run the tests for +the first time, which will do the copy automatically and use the default (sqlite3). -Make sure that you create database objects with the same user that you specified in -connection.rb otherwise (on Postgres, at least) tests for default values will fail. +You can build postgres and mysql databases using the build_postgresql and build_mysql rake tasks. -== Running with Rake +== Running the tests -The easiest way to run the unit tests is through Rake. The default task runs -the entire test suite for all the adapters. You can also run the suite on just -one adapter by using the tasks test_mysql, test_sqlite3, test_postgresql or any -of the other test_ tasks. For more information, checkout the full array of rake -tasks with "rake -T" +You can run a particular test file from the command line, e.g. -Rake can be found at http://rake.rubyforge.org + $ ruby -Itest test/cases/base_test.rb -== Running by hand +To run a specific test: -Unit tests are located in test/cases directory. If you only want to run a single test suite, -you can do so with: + $ ruby -Itest test/cases/base_test.rb -n test_something_works - rake test_mysql TEST=test/cases/base_test.rb +You can run with a database other than the default you set in test/config.yml, using the ARCONN +environment variable: -That'll run the base suite using the MySQL-Ruby adapter. Some tests rely on the schema -being initialized - you can initialize the schema with: + $ ARCONN=postgresql ruby -Itest test/cases/base_test.rb - rake test_mysql TEST=test/cases/aaa_create_tables_test.rb - rake mysql:build_databases +You can run all the tests for a given database via rake: -To setup the testing environment for PostgreSQL use this command: + $ rake test_mysql - rake postgresql:build_databases +The 'rake test' task will run all the tests for mysql, mysql2, sqlite3 and postgresql. -The incantation for running a particular test looks like this +== Identity Map - rake test TEST=test/cases/datatype_test_postgresql.rb TESTOPTS="-n test_timestamp_with_zone_values_without_rails_time_zone_support" +By default the tests run with the Identity Map turned off. But all tests should pass whether or +not the identity map is on or off. You can turn it on using the IM env variable: + $ IM=true ruby -Itest test/case/base_test.rb + +== Config file + +By default, the config file is expected to be at the path test/config.yml. You can specify a +custom location with the ARCONFIG environment variable. diff --git a/activerecord/Rakefile b/activerecord/Rakefile index e414c4fb1c..d769a73dba 100755 --- a/activerecord/Rakefile +++ b/activerecord/Rakefile @@ -1,11 +1,10 @@ #!/usr/bin/env rake require 'rake/testtask' require 'rake/packagetask' -require 'rake/gempackagetask' +require 'rubygems/package_task' require File.expand_path(File.dirname(__FILE__)) + "/test/config" - -MYSQL_DB_USER = 'rails' +require File.expand_path(File.dirname(__FILE__)) + "/test/support/config" def run_without_aborting(*tasks) errors = [] @@ -43,9 +42,8 @@ end %w( mysql mysql2 postgresql sqlite3 sqlite3_mem 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-z0-9]+/] - t.libs << "test" << connection_path + t.libs << 'test' t.test_files = (Dir.glob( "test/cases/**/*_test.rb" ).reject { |x| x =~ /\/adapters\// } + Dir.glob("test/cases/adapters/#{adapter_short}/**/*_test.rb")).sort @@ -55,21 +53,27 @@ end } task "isolated_test_#{adapter}" do - connection_path = "test/connections/#{adapter =~ /jdbc/ ? 'jdbc' : 'native'}_#{adapter}" adapter_short = adapter == 'db2' ? adapter : adapter[/^[a-z0-9]+/] - puts [adapter, adapter_short, connection_path].inspect + puts [adapter, adapter_short].inspect ruby = File.join(*RbConfig::CONFIG.values_at('bindir', 'RUBY_INSTALL_NAME')) (Dir["test/cases/**/*_test.rb"].reject { |x| x =~ /\/adapters\// } + Dir["test/cases/adapters/#{adapter_short}/**/*_test.rb"]).all? do |file| - sh(ruby, "-Ilib:test:#{connection_path}", file) + sh(ruby, "-Itest", file) end or raise "Failures" end namespace adapter do task :test => "test_#{adapter}" task :isolated_test => "isolated_test_#{adapter}" + + # Set the connection environment for the adapter + task(:env) { ENV['ARCONN'] = adapter } end + + # Make sure the adapter test evaluates the env setting task + task "test_#{adapter}" => "#{adapter}:env" + task "isolated_test_#{adapter}" => "#{adapter}:env" end rule '.sqlite3' do |t| @@ -84,14 +88,16 @@ task :test_sqlite3 => [ namespace :mysql do desc 'Build the MySQL test databases' task :build_databases do - %x( mysql --user=#{MYSQL_DB_USER} -e "create DATABASE activerecord_unittest DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_unicode_ci ") - %x( mysql --user=#{MYSQL_DB_USER} -e "create DATABASE activerecord_unittest2 DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_unicode_ci ") + config = ARTest.config['connections']['mysql'] + %x( mysql --user=#{config['arunit']['username']} -e "create DATABASE #{config['arunit']['database']} DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_unicode_ci ") + %x( mysql --user=#{config['arunit2']['username']} -e "create DATABASE #{config['arunit2']['database']} DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_unicode_ci ") end desc 'Drop the MySQL test databases' task :drop_databases do - %x( mysqladmin --user=#{MYSQL_DB_USER} -f drop activerecord_unittest ) - %x( mysqladmin --user=#{MYSQL_DB_USER} -f drop activerecord_unittest2 ) + config = ARTest.config['connections']['mysql'] + %x( mysqladmin --user=#{config['arunit']['username']} -f drop #{config['arunit']['database']} ) + %x( mysqladmin --user=#{config['arunit2']['username']} -f drop #{config['arunit2']['database']} ) end desc 'Rebuild the MySQL test databases' @@ -106,14 +112,16 @@ task :rebuild_mysql_databases => 'mysql:rebuild_databases' namespace :postgresql do desc 'Build the PostgreSQL test databases' task :build_databases do - %x( createdb -E UTF8 activerecord_unittest ) - %x( createdb -E UTF8 activerecord_unittest2 ) + config = ARTest.config['connections']['postgresql'] + %x( createdb -E UTF8 #{config['arunit']['database']} ) + %x( createdb -E UTF8 #{config['arunit2']['database']} ) end desc 'Drop the PostgreSQL test databases' task :drop_databases do - %x( dropdb activerecord_unittest ) - %x( dropdb activerecord_unittest2 ) + config = ARTest.config['connections']['postgresql'] + %x( dropdb #{config['arunit']['database']} ) + %x( dropdb #{config['arunit2']['database']} ) end desc 'Rebuild the PostgreSQL test databases' @@ -152,8 +160,9 @@ namespace :frontbase do DISCONNECT ALL; ) end - create_activerecord_unittest = build_frontbase_database['activerecord_unittest', File.join(SCHEMA_ROOT, 'frontbase.sql')] - create_activerecord_unittest2 = build_frontbase_database['activerecord_unittest2', File.join(SCHEMA_ROOT, 'frontbase2.sql')] + config = ARTest.config['connections']['frontbase'] + create_activerecord_unittest = build_frontbase_database[config['arunit']['database'], File.join(SCHEMA_ROOT, 'frontbase.sql')] + create_activerecord_unittest2 = build_frontbase_database[config['arunit2']['database'], File.join(SCHEMA_ROOT, 'frontbase2.sql')] execute_frontbase_sql = Proc.new do |sql| system(<<-SHELL) /Library/FrontBase/bin/sql92 <<-SQL @@ -171,7 +180,7 @@ task :rebuild_frontbase_databases => 'frontbase:rebuild_databases' spec = eval(File.read('activerecord.gemspec')) -Rake::GemPackageTask.new(spec) do |p| +Gem::PackageTask.new(spec) do |p| p.gem_spec = spec end diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec index 43a1258c20..8ac4d9a225 100644 --- a/activerecord/activerecord.gemspec +++ b/activerecord/activerecord.gemspec @@ -13,7 +13,7 @@ Gem::Specification.new do |s| s.email = 'david@loudthinking.com' s.homepage = 'http://www.rubyonrails.org' - s.files = Dir['CHANGELOG', 'README.rdoc', 'examples/**/*', 'lib/**/*'] + s.files = Dir['CHANGELOG', 'MIT-LICENSE', 'README.rdoc', 'examples/**/*', 'lib/**/*'] s.require_path = 'lib' s.extra_rdoc_files = %w( README.rdoc ) @@ -21,6 +21,6 @@ Gem::Specification.new do |s| s.add_dependency('activesupport', version) s.add_dependency('activemodel', version) - s.add_dependency('arel', '~> 2.1.1') - s.add_dependency('tzinfo', '~> 0.3.27') + s.add_dependency('arel', '~> 2.1.3') + s.add_dependency('tzinfo', '~> 0.3.29') end diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb index 90d3b58c78..81ddbba51e 100644 --- a/activerecord/lib/active_record/aggregations.rb +++ b/activerecord/lib/active_record/aggregations.rb @@ -176,7 +176,7 @@ module ActiveRecord # order in which mappings are defined determine the order in which attributes are sent to the # value class constructor. # * <tt>:allow_nil</tt> - Specifies that the value object will not be instantiated when all mapped - # attributes are +nil+. Setting the value object to +nil+ has the effect of writing +nil+ to all + # attributes are +nil+. Setting the value object to +nil+ has the effect of writing +nil+ to all # mapped attributes. # This defaults to +false+. # * <tt>:constructor</tt> - A symbol specifying the name of the constructor method or a Proc that diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 4979113b0b..2cbfa42718 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -76,12 +76,6 @@ module ActiveRecord end end - class HasAndBelongsToManyAssociationWithPrimaryKeyError < ActiveRecordError #:nodoc: - def initialize(reflection) - super("Primary key is not allowed in a has_and_belongs_to_many join table (#{reflection.options[:join_table]}).") - end - end - class HasAndBelongsToManyAssociationForeignKeyNeeded < ActiveRecordError #:nodoc: def initialize(reflection) super("Cannot create self referential has_and_belongs_to_many association on '#{reflection.class_name rescue nil}##{reflection.name rescue nil}'. :association_foreign_key cannot be the same as the :foreign_key.") @@ -458,20 +452,20 @@ module ActiveRecord # end # # Some extensions can only be made to work with knowledge of the association's internals. - # Extensions can access relevant state using the following methods (where 'items' is the + # Extensions can access relevant state using the following methods (where +items+ is the # name of the association): # - # * +record.association(:items).owner+ - Returns the object the association is part of. - # * +record.association(:items).reflection+ - Returns the reflection object that describes the association. - # * +record.association(:items).target+ - Returns the associated object for +belongs_to+ and +has_one+, or + # * <tt>record.association(:items).owner</tt> - Returns the object the association is part of. + # * <tt>record.association(:items).reflection</tt> - Returns the reflection object that describes the association. + # * <tt>record.association(:items).target</tt> - Returns the associated object for +belongs_to+ and +has_one+, or # the collection of associated objects for +has_many+ and +has_and_belongs_to_many+. # # === Association Join Models # # Has Many associations can be configured with the <tt>:through</tt> option to use an - # explicit join model to retrieve the data. This operates similarly to a - # +has_and_belongs_to_many+ association. The advantage is that you're able to add validations, - # callbacks, and extra attributes on the join model. Consider the following schema: + # explicit join model to retrieve the data. This operates similarly to a + # +has_and_belongs_to_many+ association. The advantage is that you're able to add validations, + # callbacks, and extra attributes on the join model. Consider the following schema: # # class Author < ActiveRecord::Base # has_many :authorships @@ -528,7 +522,7 @@ module ActiveRecord # @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 + # 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 @@ -596,7 +590,7 @@ module ActiveRecord # === Polymorphic Associations # # Polymorphic associations on models are not restricted on what types of models they - # can be associated with. Rather, they specify an interface that a +has_many+ association + # can be associated with. Rather, they specify an interface that a +has_many+ association # must adhere to. # # class Asset < ActiveRecord::Base @@ -610,7 +604,7 @@ module ActiveRecord # @asset.attachable = @post # # This works by using a type column in addition to a foreign key to specify the associated - # record. In the Asset example, you'd need an +attachable_id+ integer column and an + # record. In the Asset example, you'd need an +attachable_id+ integer column and an # +attachable_type+ string column. # # Using polymorphic associations in combination with single table inheritance (STI) is @@ -666,7 +660,7 @@ module ActiveRecord # # Consider the following loop using the class above: # - # for post in Post.all + # Post.all.each do |post| # puts "Post: " + post.title # puts "Written by: " + post.author.name # puts "Last comment on: " + post.comments.first.created_on @@ -675,7 +669,7 @@ module ActiveRecord # To iterate over these one hundred posts, we'll generate 201 database queries. Let's # first just optimize it for retrieving the author: # - # for post in Post.find(:all, :include => :author) + # Post.find(:all, :include => :author).each do |post| # # This references the name of the +belongs_to+ association that also used the <tt>:author</tt> # symbol. After loading the posts, find will collect the +author_id+ from each one and load @@ -684,7 +678,7 @@ module ActiveRecord # # We can improve upon the situation further by referencing both associations in the finder with: # - # for post in Post.find(:all, :include => [ :author, :comments ]) + # Post.find(:all, :include => [ :author, :comments ]).each do |post| # # This will load all comments with a single query. This reduces the total number of queries # to 3. More generally the number of queries will be 1 plus the number of associations @@ -692,7 +686,7 @@ module ActiveRecord # # To include a deep hierarchy of associations, use a hash: # - # for post in Post.find(:all, :include => [ :author, { :comments => { :author => :gravatar } } ]) + # Post.find(:all, :include => [ :author, { :comments => { :author => :gravatar } } ]).each do |post| # # That'll grab not only all the comments but all their authors and gravatar pictures. # You can mix and match symbols, arrays and hashes in any combination to describe the @@ -741,7 +735,7 @@ module ActiveRecord # Picture.find(:first, :include => :most_recent_comments).most_recent_comments # => returns all associated comments. # # When eager loaded, conditions are interpolated in the context of the model class, not - # the model instance. Conditions are lazily interpolated before the actual model exists. + # the model instance. Conditions are lazily interpolated before the actual model exists. # # Eager loading is supported with polymorphic associations. # @@ -765,7 +759,7 @@ module ActiveRecord # == Table Aliasing # # Active Record uses table aliasing in the case that a table is referenced multiple times - # in a join. If a table is referenced only once, the standard table name is used. The + # in a join. If a table is referenced only once, the standard table name is used. The # second time, the table is aliased as <tt>#{reflection_name}_#{parent_table_name}</tt>. # Indexes are appended for any more successive uses of the table name. # @@ -847,7 +841,7 @@ module ActiveRecord # == Bi-directional associations # # When you specify an association there is usually an association on the associated model - # that specifies the same relationship in reverse. For example, with the following models: + # that specifies the same relationship in reverse. For example, with the following models: # # class Dungeon < ActiveRecord::Base # has_many :traps @@ -864,9 +858,9 @@ module ActiveRecord # # The +traps+ association on +Dungeon+ and the +dungeon+ association on +Trap+ are # the inverse of each other and the inverse of the +dungeon+ association on +EvilWizard+ - # is the +evil_wizard+ association on +Dungeon+ (and vice-versa). By default, + # is the +evil_wizard+ association on +Dungeon+ (and vice-versa). By default, # Active Record doesn't know anything about these inverse relationships and so no object - # loading optimisation is possible. For example: + # loading optimization is possible. For example: # # d = Dungeon.first # t = d.traps.first @@ -876,8 +870,8 @@ module ActiveRecord # # The +Dungeon+ instances +d+ and <tt>t.dungeon</tt> in the above example refer to # the same object data from the database, but are actually different in-memory copies - # of that data. Specifying the <tt>:inverse_of</tt> option on associations lets you tell - # Active Record about inverse relationships and it will optimise object loading. For + # of that data. Specifying the <tt>:inverse_of</tt> option on associations lets you tell + # Active Record about inverse relationships and it will optimise object loading. For # example, if we changed our model definitions to: # # class Dungeon < ActiveRecord::Base @@ -1060,7 +1054,7 @@ module ActiveRecord # specify it with this option. # [:conditions] # Specify the conditions that the associated objects must meet in order to be included as a +WHERE+ - # SQL fragment, such as <tt>price > 5 AND name LIKE 'B%'</tt>. Record creations from + # SQL fragment, such as <tt>price > 5 AND name LIKE 'B%'</tt>. Record creations from # the association are scoped if a hash is used. # <tt>has_many :posts, :conditions => {:published => true}</tt> will create published # posts with <tt>@blog.posts.create</tt> or <tt>@blog.posts.build</tt>. @@ -1075,10 +1069,11 @@ module ActiveRecord # Specify the method that returns the primary key used for the association. By default this is +id+. # [:dependent] # If set to <tt>:destroy</tt> all the associated objects are destroyed - # alongside this object by calling their +destroy+ method. If set to <tt>:delete_all</tt> all associated - # objects are deleted *without* calling their +destroy+ method. If set to <tt>:nullify</tt> all associated + # alongside this object by calling their +destroy+ method. If set to <tt>:delete_all</tt> all associated + # objects are deleted *without* calling their +destroy+ method. If set to <tt>:nullify</tt> all associated # objects' foreign keys are set to +NULL+ *without* calling their +save+ callbacks. If set to - # <tt>:restrict</tt> this object cannot be deleted if it has any associated object. + # <tt>:restrict</tt> this object raises an <tt>ActiveRecord::DeleteRestrictionError</tt> exception and + # cannot be deleted if it has any associated objects. # # If using with the <tt>:through</tt> option, the association on the join model must be # a +belongs_to+, and the records which get deleted are the join records, rather than @@ -1227,7 +1222,8 @@ module ActiveRecord # If set to <tt>:destroy</tt>, the associated object is destroyed when this object is. If set to # <tt>:delete</tt>, the associated object is deleted *without* calling its destroy method. # If set to <tt>:nullify</tt>, the associated object's foreign key is set to +NULL+. - # Also, association is assigned. + # Also, association is assigned. If set to <tt>:restrict</tt> this object raises an + # <tt>ActiveRecord::DeleteRestrictionError</tt> exception and cannot be deleted if it has any associated object. # [:foreign_key] # Specify the foreign key used for the association. By default this is guessed to be the name # of this class in lower-case and "_id" suffixed. So a Person class that makes a +has_one+ association @@ -1243,7 +1239,7 @@ module ActiveRecord # you want to do a join but not include the joined columns. Do not forget to include the # primary and foreign keys, otherwise it will raise an error. # [:through] - # Specifies a Join Model through which to perform the query. Options for <tt>:class_name</tt>, + # Specifies a Join Model through which to perform the query. Options for <tt>:class_name</tt>, # <tt>:primary_key</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>has_one</tt> # or <tt>belongs_to</tt> association on the join model. @@ -1265,7 +1261,7 @@ module ActiveRecord # By default, only save the associated object if it's a new record. # [:inverse_of] # Specifies the name of the <tt>belongs_to</tt> association on the associated object - # that is the inverse of this <tt>has_one</tt> association. Does not work in combination + # that is the inverse of this <tt>has_one</tt> association. Does not work in combination # with <tt>:through</tt> or <tt>:as</tt> options. # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail. # @@ -1383,7 +1379,7 @@ module ActiveRecord # will be updated with the current time in addition to the updated_at/on attribute. # [:inverse_of] # Specifies the name of the <tt>has_one</tt> or <tt>has_many</tt> association on the associated - # object that is the inverse of this <tt>belongs_to</tt> association. Does not work in + # object that is the inverse of this <tt>belongs_to</tt> association. Does not work in # combination with the <tt>:polymorphic</tt> options. # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail. # @@ -1403,15 +1399,15 @@ module ActiveRecord end # Specifies a many-to-many relationship with another class. This associates two classes via an - # intermediate join table. Unless the join table is explicitly specified as an option, it is + # intermediate join table. Unless the join table is explicitly specified as an option, it is # guessed using the lexical order of the class names. So a join between Developer and Project # will give the default join table name of "developers_projects" because "D" outranks "P". - # Note that this precedence is calculated using the <tt><</tt> operator for String. This + # Note that this precedence is calculated using the <tt><</tt> operator for String. This # means that if the strings are of different lengths, and the strings are equal when compared # up to the shortest length, then the longer string is considered of higher - # lexical precedence than the shorter one. For example, one would expect the tables "paper_boxes" and "papers" + # lexical precedence than the shorter one. For example, one would expect the tables "paper_boxes" and "papers" # to generate a join table name of "papers_paper_boxes" because of the length of the name "paper_boxes", - # but it in fact generates a join table name of "paper_boxes_papers". Be aware of this caveat, and use the + # but it in fact generates a join table name of "paper_boxes_papers". Be aware of this caveat, and use the # custom <tt>:join_table</tt> option if you need to. # # The join table should not have a primary key or a model associated with it. You must manually generate the @@ -1513,7 +1509,7 @@ module ActiveRecord # the association will use "project_id" as the default <tt>:association_foreign_key</tt>. # [:conditions] # Specify the conditions that the associated object must meet in order to be included as a +WHERE+ - # SQL fragment, such as <tt>authorized = 1</tt>. Record creations from the association are + # SQL fragment, such as <tt>authorized = 1</tt>. Record creations from the association are # scoped if a hash is used. # <tt>has_many :posts, :conditions => {:published => true}</tt> will create published posts with <tt>@blog.posts.create</tt> # or <tt>@blog.posts.build</tt>. diff --git a/activerecord/lib/active_record/associations/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb index bd98cf2f46..92ed844a2e 100644 --- a/activerecord/lib/active_record/associations/alias_tracker.rb +++ b/activerecord/lib/active_record/associations/alias_tracker.rb @@ -9,7 +9,7 @@ module ActiveRecord # table_joins is an array of arel joins which might conflict with the aliases we assign here def initialize(table_joins = []) - @aliases = Hash.new + @aliases = Hash.new { |h,k| h[k] = initial_count_for(k) } @table_joins = table_joins end @@ -26,8 +26,6 @@ module ActiveRecord def aliased_name_for(table_name, aliased_name = nil) aliased_name ||= table_name - initialize_count_for(table_name) if aliases[table_name].nil? - if aliases[table_name].zero? # If it's zero, we can have our table_name aliases[table_name] = 1 @@ -36,8 +34,6 @@ module ActiveRecord # Otherwise, we need to use an alias aliased_name = connection.table_alias_for(aliased_name) - initialize_count_for(aliased_name) if aliases[aliased_name].nil? - # Update the count aliases[aliased_name] += 1 @@ -49,32 +45,24 @@ module ActiveRecord end end - def pluralize(table_name, base) - base.pluralize_table_names ? table_name.to_s.pluralize : table_name.to_s - end - private - def initialize_count_for(name) - aliases[name] = 0 - - unless Arel::Table === table_joins - # quoted_name should be downcased as some database adapters (Oracle) return quoted name in uppercase - quoted_name = connection.quote_table_name(name).downcase + def initial_count_for(name) + return 0 if Arel::Table === table_joins - aliases[name] += table_joins.map { |join| - # Table names + table aliases - join.left.downcase.scan( - /join(?:\s+\w+)?\s+(\S+\s+)?#{quoted_name}\son/ - ).size - }.sum - end + # quoted_name should be downcased as some database adapters (Oracle) return quoted name in uppercase + quoted_name = connection.quote_table_name(name).downcase - aliases[name] + table_joins.map { |join| + # Table names + table aliases + join.left.downcase.scan( + /join(?:\s+\w+)?\s+(\S+\s+)?#{quoted_name}\son/ + ).size + }.sum end def truncate(name) - name[0..connection.table_alias_length-3] + name.slice(0, connection.table_alias_length - 2) end def connection diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index 687b668634..daadc8aa81 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -141,7 +141,7 @@ module ActiveRecord @target ||= find_target end end - loaded! + loaded! unless loaded? target rescue ActiveRecord::RecordNotFound reset @@ -177,9 +177,7 @@ module ActiveRecord # Sets the owner attributes on the given record def set_owner_attributes(record) - if owner.persisted? - creation_attributes.each { |key, value| record[key] = value } - end + creation_attributes.each { |key, value| record[key] = value } end # Should be true if there is a foreign key present on the owner which @@ -226,6 +224,15 @@ module ActiveRecord def association_class @reflection.klass end + + def build_record(attributes, options) + reflection.build_association(attributes, options) do |record| + record.assign_attributes( + create_scope.except(*record.changed), + :without_protection => true + ) + end + end end end end diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index 94847bc2ae..9e6d9e73c5 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -60,7 +60,7 @@ module ActiveRecord scope = scope.joins(join( join_table, - table[reflection.active_record_primary_key]. + table[reflection.association_primary_key]. eq(join_table[reflection.association_foreign_key]) )) diff --git a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb index 4b48757da7..d7632b2ea9 100644 --- a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb @@ -39,10 +39,6 @@ module ActiveRecord::Associations::Builder model.send(:undecorated_table_name, model.to_s), model.send(:undecorated_table_name, reflection.class_name) ) - - if model.connection.supports_primary_key? && (model.connection.primary_key(reflection.options[:join_table]) rescue false) - raise ActiveRecord::HasAndBelongsToManyAssociationWithPrimaryKeyError.new(reflection) - end end # Generates a join table name from two provided table names. diff --git a/activerecord/lib/active_record/associations/builder/singular_association.rb b/activerecord/lib/active_record/associations/builder/singular_association.rb index 638a2ec72a..0cbbba041a 100644 --- a/activerecord/lib/active_record/associations/builder/singular_association.rb +++ b/activerecord/lib/active_record/associations/builder/singular_association.rb @@ -13,19 +13,6 @@ module ActiveRecord::Associations::Builder private - def define_readers - super - name = self.name - - model.redefine_method("#{name}_loaded?") do - ActiveSupport::Deprecation.warn( - "Calling obj.#{name}_loaded? is deprecated. Please use " \ - "obj.association(:#{name}).loaded? instead." - ) - association(name).loaded? - end - end - def define_constructors name = self.name diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 902ad8cb64..c15ee18e53 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -78,10 +78,14 @@ module ActiveRecord end def find(*args) - if options[:finder_sql] - find_by_scan(*args) + if block_given? + load_target.find(*args) { |*block_args| yield(*block_args) } else - scoped.find(*args) + if options[:finder_sql] + find_by_scan(*args) + else + scoped.find(*args) + end end end @@ -104,44 +108,23 @@ module ActiveRecord end def create(attributes = {}, options = {}, &block) - unless owner.persisted? - raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved" - end - - if attributes.is_a?(Array) - attributes.collect { |attr| create(attr, options, &block) } - else - transaction do - add_to_target(build_record(attributes, options)) do |record| - yield(record) if block_given? - insert_record(record) - end - end - end + create_record(attributes, options, &block) end - def create!(attrs = {}, options = {}, &block) - record = create(attrs, options, &block) - Array.wrap(record).each(&:save!) - record + def create!(attributes = {}, options = {}, &block) + create_record(attributes, options, true, &block) end - # Add +records+ to this association. Returns +self+ so method calls may be chained. + # Add +records+ to this association. Returns +self+ so method calls may be chained. # Since << flattens its argument list and inserts each record, +push+ and +concat+ behave identically. def concat(*records) - result = true load_target if owner.new_record? - transaction do - records.flatten.each do |record| - raise_on_type_mismatch(record) - add_to_target(record) do |r| - result &&= insert_record(record) unless owner.new_record? - end - end + if owner.new_record? + concat_records(records) + else + transaction { concat_records(records) } end - - result && records end # Starts a transaction in the association class's database connection. @@ -310,14 +293,10 @@ module ActiveRecord other_array.each { |val| raise_on_type_mismatch(val) } original_target = load_target.dup - transaction do - delete(target - other_array) - - unless concat(other_array - target) - @target = original_target - raise RecordNotSaved, "Failed to replace #{reflection.name} because one or more of the " \ - "new records could not be saved." - end + if owner.new_record? + replace_records(other_array, original_target) + else + transaction { replace_records(other_array, original_target) } end end @@ -402,9 +381,13 @@ module ActiveRecord return memory if persisted.empty? persisted.map! do |record| - mem_record = memory.delete(record) + # Unfortunately we cannot simply do memory.delete(record) since on 1.8 this returns + # record rather than memory.at(memory.index(record)). The behaviour is fixed in 1.9. + mem_index = memory.index(record) + + if mem_index + mem_record = memory.delete_at(mem_index) - if mem_record (record.attribute_names - mem_record.changes.keys).each do |name| mem_record[name] = record[name] end @@ -418,8 +401,25 @@ module ActiveRecord persisted + memory end + def create_record(attributes, options, raise = false, &block) + unless owner.persisted? + raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved" + end + + if attributes.is_a?(Array) + attributes.collect { |attr| create_record(attr, options, raise, &block) } + else + transaction do + add_to_target(build_record(attributes, options)) do |record| + yield(record) if block_given? + insert_record(record, true, raise) + end + end + end + end + # Do the relevant stuff to insert the given record into the association collection. - def insert_record(record, validate = true) + def insert_record(record, validate = true, raise = false) raise NotImplementedError end @@ -427,26 +427,25 @@ module ActiveRecord scoped.scope_for_create.stringify_keys end - def build_record(attributes, options) - record = reflection.build_association(attributes, options) - record.assign_attributes(create_scope.except(*record.changed), :without_protection => true) - record.assign_attributes(attributes, options) - record - end - def delete_or_destroy(records, method) records = records.flatten records.each { |record| raise_on_type_mismatch(record) } existing_records = records.reject { |r| r.new_record? } - transaction do - records.each { |record| callback(:before_remove, record) } + if existing_records.empty? + remove_records(existing_records, records, method) + else + transaction { remove_records(existing_records, records, method) } + end + end - delete_records(existing_records, method) if existing_records.any? - records.each { |record| target.delete(record) } + def remove_records(existing_records, records, method) + records.each { |record| callback(:before_remove, record) } - records.each { |record| callback(:after_remove, record) } - end + delete_records(existing_records, method) if existing_records.any? + records.each { |record| target.delete(record) } + + records.each { |record| callback(:after_remove, record) } end # Delete the given records from the association, using one of the methods :destroy, @@ -455,6 +454,29 @@ module ActiveRecord raise NotImplementedError end + def replace_records(new_target, original_target) + delete(target - new_target) + + unless concat(new_target - target) + @target = original_target + raise RecordNotSaved, "Failed to replace #{reflection.name} because one or more of the " \ + "new records could not be saved." + end + end + + def concat_records(records) + result = true + + records.flatten.each do |record| + raise_on_type_mismatch(record) + add_to_target(record) do |r| + result &&= insert_record(record) unless owner.new_record? + end + end + + result && records + end + def callback(method, record) callbacks_for(method).each do |callback| case callback diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index 8415942a1a..7f7dec467a 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -56,16 +56,18 @@ module ActiveRecord Array.wrap(association.options[:extend]).each { |ext| proxy_extend(ext) } end - def respond_to?(*args) + alias_method :new, :build + + def respond_to?(name, include_private = false) super || - (load_target && target.respond_to?(*args)) || - @association.klass.respond_to?(*args) + (load_target && target.respond_to?(name, include_private)) || + @association.klass.respond_to?(name, include_private) end def method_missing(method, *args, &block) match = DynamicFinderMatch.match(method) if match && match.instantiator? - record = send(:find_or_instantiator_by_attributes, match, match.attribute_names, *args) do |r| + send(:find_or_instantiator_by_attributes, match, match.attribute_names, *args) do |r| @association.send :set_owner_attributes, r @association.send :add_to_target, r yield(r) if block_given? @@ -115,38 +117,6 @@ module ActiveRecord @association.reload self end - - def new(*args, &block) - if @association.is_a?(HasManyThroughAssociation) - @association.build(*args, &block) - else - method_missing(:new, *args, &block) - end - end - - def proxy_owner - ActiveSupport::Deprecation.warn( - "Calling record.#{@association.reflection.name}.proxy_owner is deprecated. Please use " \ - "record.association(:#{@association.reflection.name}).owner instead." - ) - @association.owner - end - - def proxy_target - ActiveSupport::Deprecation.warn( - "Calling record.#{@association.reflection.name}.proxy_target is deprecated. Please use " \ - "record.association(:#{@association.reflection.name}).target instead." - ) - @association.target - end - - def proxy_reflection - ActiveSupport::Deprecation.warn( - "Calling record.#{@association.reflection.name}.proxy_reflection is deprecated. Please use " \ - "record.association(:#{@association.reflection.name}).reflection instead." - ) - @association.reflection - end end end 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 217213808b..f7ce70db1a 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 @@ -9,8 +9,14 @@ module ActiveRecord super end - def insert_record(record, validate = true) - return if record.new_record? && !record.save(:validate => validate) + def insert_record(record, validate = true, raise = false) + if record.new_record? + if raise + record.save!(:validate => validate) + else + return unless record.save(:validate => validate) + end + end if options[:insert_sql] owner.connection.insert(interpolate(options[:insert_sql], record)) diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 78c5c4b870..50ee60284c 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -7,9 +7,14 @@ module ActiveRecord # is provided by its child HasManyThroughAssociation. class HasManyAssociation < CollectionAssociation #:nodoc: - def insert_record(record, validate = true) + def insert_record(record, validate = true, raise = false) set_owner_attributes(record) - record.save(:validate => validate) + + if raise + record.save!(:validate => validate) + else + record.save(:validate => validate) + end end private @@ -18,7 +23,7 @@ module ActiveRecord # # If the association has a counter cache it gets that value. Otherwise # it will attempt to do a count via SQL, bounded to <tt>:limit</tt> if - # there's one. Some configuration options like :group make it impossible + # there's one. Some configuration options like :group make it impossible # to do an SQL count, in those cases the array count will be used. # # That does not depend on whether the collection has already been loaded 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 7708228d23..2e818dca5d 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -6,8 +6,6 @@ module ActiveRecord class HasManyThroughAssociation < HasManyAssociation #:nodoc: include ThroughAssociation - alias_method :new, :build - # Returns the size of the collection by executing a SELECT COUNT(*) query if the collection hasn't been # loaded and calling collection.size if it has. If it's more likely than not that the collection does # have a size larger than zero, and you need to fetch that collection afterwards, it'll take one fewer @@ -33,9 +31,16 @@ module ActiveRecord super end - def insert_record(record, validate = true) + def insert_record(record, validate = true, raise = false) ensure_not_nested - return if record.new_record? && !record.save(:validate => validate) + + if record.new_record? + if raise + record.save!(:validate => validate) + else + return unless record.save(:validate => validate) + end + end through_record(record).save! update_counter(1) diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 2f3a6e71f1..2131edbc20 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -14,12 +14,12 @@ module ActiveRecord end if record - set_inverse_instance(record) set_owner_attributes(record) + set_inverse_instance(record) if owner.persisted? && save && !record.save nullify_owner_attributes(record) - set_owner_attributes(target) + set_owner_attributes(target) if target raise RecordNotSaved, "Failed to save the new associated #{reflection.name}." end end diff --git a/activerecord/lib/active_record/associations/join_helper.rb b/activerecord/lib/active_record/associations/join_helper.rb index 87e33891a5..f83138195c 100644 --- a/activerecord/lib/active_record/associations/join_helper.rb +++ b/activerecord/lib/active_record/associations/join_helper.rb @@ -32,8 +32,7 @@ module ActiveRecord end def table_alias_for(reflection, join = false) - name = alias_tracker.pluralize(reflection.name, reflection.active_record) - name << "_#{alias_suffix}" + name = "#{reflection.plural_name}_#{alias_suffix}" name << "_join" if join name end diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index 7256dd5288..779f8164cc 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -68,7 +68,8 @@ module ActiveRecord private def associated_records_by_owner - owner_keys = owners.map { |owner| owner[owner_key_name] }.compact.uniq + owners_map = owners_by_key + owner_keys = owners_map.keys.compact if klass.nil? || owner_keys.empty? records = [] @@ -84,7 +85,7 @@ module ActiveRecord records.each do |record| owner_key = record[association_key_name].to_s - owners_by_key[owner_key].each do |owner| + owners_map[owner_key].each do |owner| records_by_owner[owner] << record end end diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb index ce1f2a5543..6b010064d5 100644 --- a/activerecord/lib/active_record/associations/singular_association.rb +++ b/activerecord/lib/active_record/associations/singular_association.rb @@ -26,8 +26,7 @@ module ActiveRecord end def build(attributes = {}, options = {}) - record = reflection.build_association(attributes, options) - record.assign_attributes(create_scope.except(*record.changed), :without_protection => true) + record = build_record(attributes, options) yield(record) if block_given? set_new_record(record) record diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index 53c5c3cedf..81172179e0 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -16,7 +16,7 @@ module ActiveRecord chain[1..-1].each do |reflection| scope = scope.merge( reflection.klass.scoped.with_default_scope. - except(:select, :create_with) + except(:select, :create_with, :includes) ) end scope diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 5833c65893..d0687ed0b6 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -61,7 +61,7 @@ module ActiveRecord end end - def respond_to?(*args) + def respond_to?(name, include_private = false) self.class.define_attribute_methods unless self.class.attribute_methods_generated? super end diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb index 5f06452247..e3f221c773 100644 --- a/activerecord/lib/active_record/attribute_methods/primary_key.rb +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -48,7 +48,7 @@ module ActiveRecord end attr_accessor :original_primary_key - + # Attribute writer for the primary key column def primary_key=(value) @quoted_primary_key = nil diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index aef99e3129..9a50a20fbc 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -58,7 +58,7 @@ module ActiveRecord generated_attribute_methods.module_eval("def _#{attr_name}; #{access_code}; end; alias #{attr_name} _#{attr_name}", __FILE__, __LINE__) end - # Define an attribute reader method. Cope with nil column. + # Define an attribute reader method. Cope with nil column. # method_name is the same as attr_name except when a non-standard primary key is used, # we still define #id as an accessor for the key def define_read_method(method_name, attr_name, column) @@ -99,8 +99,9 @@ module ActiveRecord # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example, # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)). def read_attribute(attr_name) - if respond_to? "_#{attr_name}" - send "_#{attr_name}" if @attributes.has_key?(attr_name.to_s) + method = "_#{attr_name}" + if respond_to? method + send method if @attributes.has_key?(attr_name.to_s) else _read_attribute attr_name end diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index 48dbe0838a..036d514a03 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -161,7 +161,7 @@ module ActiveRecord # # For performance reasons, we don't check whether to validate at runtime. # However the validation and callback methods are lazy and those methods - # get created when they are invoked for the very first time. However, + # get created when they are invoked for the very first time. However, # this can change, for instance, when using nested attributes, which is # called _after_ the association has been defined. Since we don't want # the callbacks to get defined multiple times, there are guards that diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index fa33f02ae0..eb4a16ecf5 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -655,8 +655,8 @@ module ActiveRecord #:nodoc: def set_table_name(value = nil, &block) @quoted_table_name = nil define_attr_method :table_name, value, &block + @arel_table = nil - @arel_table = Arel::Table.new(table_name, arel_engine) @relation = Relation.new(self, arel_table) end alias :table_name= :set_table_name @@ -709,6 +709,12 @@ module ActiveRecord #:nodoc: connection_pool.columns_hash[table_name] end + # Returns a hash where the keys are column names and the values are + # default values when instantiating the AR object for this table. + def column_defaults + connection_pool.column_defaults[table_name] + end + # Returns an array of column names as strings. def column_names @column_names ||= columns.map { |column| column.name } @@ -888,7 +894,7 @@ module ActiveRecord #:nodoc: end def arel_table - Arel::Table.new(table_name, arel_engine) + @arel_table ||= Arel::Table.new(table_name, arel_engine) end def arel_engine @@ -1204,11 +1210,11 @@ MSG end def current_scope #:nodoc: - Thread.current[:"#{self}_current_scope"] + Thread.current["#{self}_current_scope"] end def current_scope=(scope) #:nodoc: - Thread.current[:"#{self}_current_scope"] = scope + Thread.current["#{self}_current_scope"] = scope end # Use this macro in your model to set a default scope for all operations on @@ -1261,11 +1267,14 @@ MSG self.default_scopes = default_scopes + [scope] end + # The @ignore_default_scope flag is used to prevent an infinite recursion situation where + # a default scope references a scope which has a default scope which references a scope... def build_default_scope #:nodoc: + return if defined?(@ignore_default_scope) && @ignore_default_scope + @ignore_default_scope = true + if method(:default_scope).owner != Base.singleton_class - # Use relation.scoping to ensure we ignore whatever the current value of - # self.current_scope may be. - relation.scoping { default_scope } + default_scope elsif default_scopes.any? default_scopes.inject(relation) do |default_scope, scope| if scope.is_a?(Hash) @@ -1277,6 +1286,8 @@ MSG end end end + ensure + @ignore_default_scope = false end # Returns the class type of the record using the current module as a prefix. So descendants of @@ -1299,7 +1310,6 @@ MSG rescue NameError => e # We don't want to swallow NoMethodError < NameError errors raise e unless e.instance_of?(NameError) - rescue ArgumentError end end @@ -1523,6 +1533,7 @@ MSG @marked_for_destruction = false @previously_changed = {} @changed_attributes = {} + @relation = nil ensure_proper_type set_serialized_attributes @@ -1531,9 +1542,8 @@ MSG assign_attributes(attributes, options) if attributes - result = yield self if block_given? + yield self if block_given? run_callbacks :initialize - result end # Populate +coder+ with attributes about this record that should be @@ -1564,6 +1574,7 @@ MSG # post.title # => 'hello world' def init_with(coder) @attributes = coder['attributes'] + @relation = nil set_serialized_attributes @@ -1653,9 +1664,6 @@ MSG # If any attributes are protected by either +attr_protected+ or # +attr_accessible+ then only settable attributes will be assigned. # - # The +guard_protected_attributes+ argument is now deprecated, use - # the +assign_attributes+ method if you want to bypass mass-assignment security. - # # class User < ActiveRecord::Base # attr_protected :is_admin # end @@ -1664,20 +1672,10 @@ MSG # user.attributes = { :username => 'Phusion', :is_admin => true } # user.username # => "Phusion" # user.is_admin? # => false - def attributes=(new_attributes, guard_protected_attributes = nil) - unless guard_protected_attributes.nil? - message = "the use of 'guard_protected_attributes' will be removed from the next major release of rails, " + - "if you want to bypass mass-assignment security then look into using assign_attributes" - ActiveSupport::Deprecation.warn(message) - end - + def attributes=(new_attributes) return unless new_attributes.is_a?(Hash) - if guard_protected_attributes == false - assign_attributes(new_attributes, :without_protection => true) - else - assign_attributes(new_attributes) - end + assign_attributes(new_attributes) end # Allows you to set all the attributes for a particular mass-assignment @@ -1711,12 +1709,11 @@ MSG return unless new_attributes attributes = new_attributes.stringify_keys - role = options[:as] || :default - multi_parameter_attributes = [] + @mass_assignment_options = options unless options[:without_protection] - attributes = sanitize_for_mass_assignment(attributes, role) + attributes = sanitize_for_mass_assignment(attributes, mass_assignment_role) end attributes.each do |k, v| @@ -1729,6 +1726,7 @@ MSG end end + @mass_assignment_options = nil assign_multiparameter_attributes(multi_parameter_attributes) end @@ -1783,16 +1781,12 @@ MSG # Note also that destroying a record preserves its ID in the model instance, so deleted # models are still comparable. def ==(comparison_object) - comparison_object.equal?(self) || + super || comparison_object.instance_of?(self.class) && id.present? && comparison_object.id == id end - - # Delegates to == - def eql?(comparison_object) - self == comparison_object - end + alias :eql? :== # Delegates to id in order to allow two records of the same type and id to work with something like: # [ Person.find(1), Person.find(2), Person.find(3) ] & [ Person.find(1), Person.find(4) ] # => [ Person.find(1) ] @@ -1810,6 +1804,15 @@ MSG @attributes.frozen? end + # Allows sort on objects + def <=>(other_object) + if other_object.is_a?(self.class) + self.to_key <=> other_object.to_key + else + nil + end + end + # Backport dup from 1.9 so that initialize_dup() gets called unless Object.respond_to?(:initialize_dup) def dup # :nodoc: @@ -1861,12 +1864,16 @@ MSG # Returns the contents of the record as a nicely formatted string. def inspect - attributes_as_nice_string = self.class.column_names.collect { |name| - if has_attribute?(name) - "#{name}: #{attribute_for_inspect(name)}" - end - }.compact.join(", ") - "#<#{self.class} #{attributes_as_nice_string}>" + inspection = if @attributes + self.class.column_names.collect { |name| + if has_attribute?(name) + "#{name}: #{attribute_for_inspect(name)}" + end + }.compact.join(", ") + else + "not initialized" + end + "#<#{self.class} #{inspection}>" end protected @@ -1884,12 +1891,33 @@ MSG value end + def mass_assignment_options + @mass_assignment_options ||= {} + end + + def mass_assignment_role + mass_assignment_options[:as] || :default + end + private + # Under Ruby 1.9, Array#flatten will call #to_ary (recursively) on each of the elements + # of the array, and then rescues from the possible NoMethodError. If those elements are + # ActiveRecord::Base's, then this triggers the various method_missing's that we have, + # which significantly impacts upon performance. + # + # So we can avoid the method_missing hit by explicitly defining #to_ary as nil here. + # + # See also http://tenderlovemaking.com/2011/06/28/til-its-ok-to-return-nil-from-to_ary/ + def to_ary # :nodoc: + nil + end + def set_serialized_attributes - (@attributes.keys & self.class.serialized_attributes.keys).each do |key| - coder = self.class.serialized_attributes[key] - @attributes[key] = coder.load @attributes[key] + sattrs = self.class.serialized_attributes + + sattrs.each do |key, coder| + @attributes[key] = coder.load @attributes[key] if @attributes.key?(key) end end @@ -1899,8 +1927,9 @@ MSG # do Reply.new without having to set <tt>Reply[Reply.inheritance_column] = "Reply"</tt> yourself. # No such attribute would be set for objects of the Message class in that example. def ensure_proper_type - unless self.class.descends_from_active_record? - write_attribute(self.class.inheritance_column, self.class.sti_name) + klass = self.class + if klass.finder_needs_type_condition? + write_attribute(klass.inheritance_column, klass.sti_name) end end @@ -2007,7 +2036,7 @@ MSG set_values = (1..3).collect{|position| values_hash_from_param[position].blank? ? 1 : values_hash_from_param[position]} begin Date.new(*set_values) - rescue ArgumentError => ex # if Date.new raises an exception on an invalid date + rescue ArgumentError # if Date.new raises an exception on an invalid date instantiate_time_object(name, set_values).to_date # we instantiate Time object and convert it back to a date thus using Time's logic in handling invalid dates end end @@ -2074,8 +2103,10 @@ MSG end def populate_with_current_scope_attributes - self.class.scoped.scope_for_create.each do |att,value| - respond_to?("#{att}=") && send("#{att}=", value) + return unless self.class.scope_attributes? + + self.class.scope_attributes.each do |att,value| + send("#{att}=", value) if respond_to?("#{att}=") end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index 3fdbc9fce5..ddfdb05297 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -60,6 +60,7 @@ module ActiveRecord attr_accessor :automatic_reconnect attr_reader :spec, :connections attr_reader :columns, :columns_hash, :primary_keys, :tables + attr_reader :column_defaults # Creates a new ConnectionPool object. +spec+ is a ConnectionSpecification # object which describes database connection information (e.g. adapter, @@ -106,6 +107,12 @@ module ActiveRecord }] end + @column_defaults = Hash.new do |h, table_name| + h[table_name] = Hash[columns[table_name].map { |col| + [col.name, col.default] + }] + end + @primary_keys = Hash.new do |h, table_name| h[table_name] = with_connection do |conn| table_exists?(table_name) ? conn.primary_key(table_name) : 'id' @@ -119,6 +126,7 @@ module ActiveRecord with_connection do |conn| conn.tables.each { |table| @tables[table] = true } + @tables[name] = true if !@tables.key?(name) && conn.table_exists?(name) end @tables.key? name @@ -132,6 +140,7 @@ module ActiveRecord def clear_cache! @columns.clear @columns_hash.clear + @column_defaults.clear @tables.clear end @@ -139,6 +148,7 @@ module ActiveRecord def clear_table_cache!(table_name) @columns.delete table_name @columns_hash.delete table_name + @column_defaults.delete table_name @primary_keys.delete table_name end @@ -165,7 +175,7 @@ module ActiveRecord checkin conn if conn end - # If a connection already exists yield it to the block. If no connection + # If a connection already exists yield it to the block. If no connection # exists checkout a connection, yield it to the block, and checkin the # connection when finished. def with_connection @@ -425,6 +435,14 @@ module ActiveRecord @testing = testing end + def method_missing(method_sym, *arguments, &block) + @body.send(method_sym, *arguments, &block) + end + + def respond_to?(method_sym, include_private = false) + super || @body.respond_to?(method_sym) + end + def each(&block) body.each(&block) end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index b3eb23bbb3..777ef15dfc 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -1,5 +1,3 @@ -require 'active_support/core_ext/module/deprecation' - module ActiveRecord module ConnectionAdapters # :nodoc: module DatabaseStatements @@ -42,7 +40,7 @@ module ActiveRecord undef_method :execute # Executes +sql+ statement in the context of this connection using - # +binds+ as the bind substitutes. +name+ is logged along with + # +binds+ as the bind substitutes. +name+ is logged along with # the executed +sql+ statement. def exec_query(sql, name = 'SQL', binds = []) end @@ -245,38 +243,13 @@ module ActiveRecord # done if the transaction block raises an exception or returns false. def rollback_db_transaction() end - # Appends +LIMIT+ and +OFFSET+ options to an SQL statement, or some SQL - # fragment that has the same semantics as LIMIT and OFFSET. - # - # +options+ must be a Hash which contains a +:limit+ option - # and an +:offset+ option. - # - # This method *modifies* the +sql+ parameter. - # - # This method is deprecated!! Stop using it! - # - # ===== Examples - # add_limit_offset!('SELECT * FROM suppliers', {:limit => 10, :offset => 50}) - # generates - # SELECT * FROM suppliers LIMIT 10 OFFSET 50 - def add_limit_offset!(sql, options) - if limit = options[:limit] - sql << " LIMIT #{sanitize_limit(limit)}" - end - if offset = options[:offset] - sql << " OFFSET #{offset.to_i}" - end - sql - end - deprecate :add_limit_offset! - def default_sequence_name(table, column) nil end # Set the sequence to the max value of the table's column. def reset_sequence!(table, column, sequence = nil) - # Do nothing by default. Implement for PostgreSQL, Oracle, ... + # Do nothing by default. Implement for PostgreSQL, Oracle, ... end # Inserts the given fixture into the table. Overridden in adapters that require @@ -308,10 +281,10 @@ module ActiveRecord # Sanitizes the given LIMIT parameter in order to prevent SQL injection. # # The +limit+ may be anything that can evaluate to a string via #to_s. It - # should look like an integer, or a comma-delimited list of integers, or + # should look like an integer, or a comma-delimited list of integers, or # an Arel SQL literal. # - # Returns Integer and Arel::Nodes::SqlLiteral limits as is. + # Returns Integer and Arel::Nodes::SqlLiteral limits as is. # Returns the sanitized limit parameter, either as an integer, or as a # string which contains a comma-delimited list of integers. def sanitize_limit(limit) 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 70a8f6bb58..82f564e41d 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -328,7 +328,7 @@ module ActiveRecord end # Checks to see if a column exists. See SchemaStatements#column_exists? - def column_exists?(column_name, type = nil, options = nil) + def column_exists?(column_name, type = nil, options = {}) @base.column_exists?(@table_name, column_name, type, options) end @@ -386,13 +386,13 @@ module ActiveRecord # Removes the given index from the table. # # ===== Examples - # ====== Remove the suppliers_name_index in the suppliers table - # t.remove_index :name - # ====== Remove the index named accounts_branch_id_index in the accounts table + # ====== Remove the index_table_name_on_column in the table_name table + # t.remove_index :column + # ====== Remove the index named index_table_name_on_branch_id in the table_name table # t.remove_index :column => :branch_id - # ====== Remove the index named accounts_branch_id_party_id_index in the accounts table + # ====== Remove the index named index_table_name_on_branch_id_and_party_id in the table_name table # t.remove_index :column => [:branch_id, :party_id] - # ====== Remove the index named by_branch_party in the accounts table + # ====== Remove the index named by_branch_party in the table_name table # t.remove_index :name => :by_branch_party def remove_index(options = {}) @base.remove_index(@table_name, options) 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 b4969c29d2..8e3ba1297e 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -4,7 +4,7 @@ module ActiveRecord module ConnectionAdapters # :nodoc: module SchemaStatements # Returns a Hash of mappings from the abstract data types to the native - # database types. See TableDefinition#column for details on the recognized + # database types. See TableDefinition#column for details on the recognized # abstract data types. def native_database_types {} @@ -78,7 +78,7 @@ module ActiveRecord # Creates a new table with the name +table_name+. +table_name+ may either # be a String or a Symbol. # - # There are two ways to work with +create_table+. You can use the block + # There are two ways to work with +create_table+. You can use the block # form or the regular form, like this: # # === Block form @@ -161,7 +161,7 @@ module ActiveRecord yield td if block_given? if options[:force] && table_exists?(table_name) - drop_table(table_name, options) + drop_table(table_name) end create_sql = "CREATE#{' TEMPORARY' if options[:temporary]} TABLE " @@ -253,7 +253,7 @@ module ActiveRecord end # Drops a table from the database. - def drop_table(table_name, options = {}) + def drop_table(table_name) execute "DROP TABLE #{quote_table_name(table_name)}" end @@ -299,7 +299,7 @@ module ActiveRecord raise NotImplementedError, "rename_column is not implemented" end - # Adds a new index to the table. +column_name+ can be a single Symbol, or + # Adds a new index to the table. +column_name+ can be a single Symbol, or # an Array of Symbols. # # The index will be named after the table and the first column name, @@ -346,11 +346,11 @@ module ActiveRecord # Remove the given index from the table. # - # Remove the suppliers_name_index in the suppliers table. - # remove_index :suppliers, :name - # Remove the index named accounts_branch_id_index in the accounts table. + # Remove the index_accounts_on_column in the accounts table. + # remove_index :accounts, :column + # Remove the index named index_accounts_on_branch_id in the accounts table. # remove_index :accounts, :column => :branch_id - # Remove the index named accounts_branch_id_party_id_index in the accounts table. + # Remove the index named index_accounts_on_branch_id_and_party_id in the accounts table. # remove_index :accounts, :column => [:branch_id, :party_id] # Remove the index named by_branch_party in the accounts table. # remove_index :accounts, :name => :by_branch_party diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index 3eddb69e73..a7856539b7 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -1,3 +1,5 @@ +require 'set' + module ActiveRecord # :stopdoc: module ConnectionAdapters diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index bbaac29e5a..d6c167ad36 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -1,6 +1,6 @@ # encoding: utf-8 -gem 'mysql2', '~> 0.3.0' +gem 'mysql2', '~> 0.3.6' require 'mysql2' module ActiveRecord @@ -347,19 +347,6 @@ module ActiveRecord execute("RELEASE SAVEPOINT #{current_savepoint_name}") end - def add_limit_offset!(sql, options) - limit, offset = options[:limit], options[:offset] - if limit && offset - sql << " LIMIT #{offset.to_i}, #{sanitize_limit(limit)}" - elsif limit - sql << " LIMIT #{sanitize_limit(limit)}" - elsif offset - sql << " OFFSET #{offset.to_i}" - end - sql - end - deprecate :add_limit_offset! - # SCHEMA STATEMENTS ======================================== def structure_dump @@ -440,10 +427,6 @@ module ActiveRecord tables(nil, schema).include? table end - def drop_table(table_name, options = {}) - super(table_name, options) - end - # Returns an array of indexes for the given table. def indexes(table_name, name = nil) indexes = [] @@ -582,11 +565,6 @@ module ActiveRecord pk_and_sequence && pk_and_sequence.first end - def case_sensitive_equality_operator - "= BINARY" - end - deprecate :case_sensitive_equality_operator - def case_sensitive_modifier(node) Arel::Nodes::Bin.new(node) end diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index a3704fe54a..9e6cb13cca 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -406,7 +406,7 @@ module ActiveRecord def exec_without_stmt(sql, name = 'SQL') # :nodoc: # Some queries, like SHOW CREATE TABLE don't work through the prepared - # statement API. For those queries, we need to use this method. :'( + # statement API. For those queries, we need to use this method. :'( log(sql, name) do result = @connection.query(sql) cols = [] @@ -487,19 +487,6 @@ module ActiveRecord execute("RELEASE SAVEPOINT #{current_savepoint_name}") end - def add_limit_offset!(sql, options) #:nodoc: - limit, offset = options[:limit], options[:offset] - if limit && offset - sql << " LIMIT #{offset.to_i}, #{sanitize_limit(limit)}" - elsif limit - sql << " LIMIT #{sanitize_limit(limit)}" - elsif offset - sql << " OFFSET #{offset.to_i}" - end - sql - end - deprecate :add_limit_offset! - # SCHEMA STATEMENTS ======================================== def structure_dump #:nodoc: @@ -581,10 +568,6 @@ module ActiveRecord tables(nil, schema).include? table end - def drop_table(table_name, options = {}) - super(table_name, options) - end - # Returns an array of indexes for the given table. def indexes(table_name, name = nil)#:nodoc: indexes = [] @@ -712,11 +695,6 @@ module ActiveRecord pk_and_sequence && pk_and_sequence.first end - def case_sensitive_equality_operator - "= BINARY" - end - deprecate :case_sensitive_equality_operator - def case_sensitive_modifier(node) Arel::Nodes::Bin.new(node) end @@ -729,7 +707,7 @@ module ActiveRecord def quoted_columns_for_index(column_names, options = {}) length = options[:length] if options.is_a?(Hash) - quoted_column_names = case length + case length when Hash column_names.map {|name| length[name] ? "#{quote_column_name(name)}(#{length[name]})" : quote_column_name(name) } when Fixnum @@ -832,7 +810,7 @@ module ActiveRecord stmt.execute(*binds.map { |col, val| type_cast(val, col) }) rescue Mysql::Error => e # Older versions of MySQL leave the prepared statement in a bad - # place when an error occurs. To support older mysql versions, we + # place when an error occurs. To support older mysql versions, we # need to close the statement and delete the statement from the # cache. stmt.close diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index f94d52e20e..29dd0a9ea3 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -198,7 +198,7 @@ module ActiveRecord # * <tt>:password</tt> - Defaults to nothing. # * <tt>:database</tt> - The name of the database. No default, must be provided. # * <tt>:schema_search_path</tt> - An optional schema search path for the connection given - # as a string of comma-separated schema names. This is backward-compatible with the <tt>:schema_order</tt> option. + # as a string of comma-separated schema names. This is backward-compatible with the <tt>:schema_order</tt> option. # * <tt>:encoding</tt> - An optional client encoding that is used in a <tt>SET client_encoding TO # <encoding></tt> call on the connection. # * <tt>:min_messages</tt> - An optional client min messages that is used in a @@ -466,10 +466,11 @@ module ActiveRecord # Executes an INSERT query and returns the new record's ID def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) - # Extract the table from the insert sql. Yuck. - _, table = extract_schema_and_table(sql.split(" ", 4)[2]) - - pk ||= primary_key(table) + unless pk + # Extract the table from the insert sql. Yuck. + table_ref = extract_table_ref_from_insert_sql(sql) + pk = primary_key(table_ref) if table_ref + end if pk select_value("#{sql} RETURNING #{quote_column_name(pk)}") @@ -565,9 +566,9 @@ module ActiveRecord def sql_for_insert(sql, pk, id_value, sequence_name, binds) unless pk - _, table = extract_schema_and_table(sql.split(" ", 4)[2]) - - pk = primary_key(table) + # Extract the table from the insert sql. Yuck. + table_ref = extract_table_ref_from_insert_sql(sql) + pk = primary_key(table_ref) if table_ref end sql = "#{sql} RETURNING #{quote_column_name(pk)}" if pk @@ -618,7 +619,7 @@ module ActiveRecord create_database(name) end - # Create a new PostgreSQL database. Options include <tt>:owner</tt>, <tt>:template</tt>, + # Create a new PostgreSQL database. Options include <tt>:owner</tt>, <tt>:template</tt>, # <tt>:encoding</tt>, <tt>:tablespace</tt>, and <tt>:connection_limit</tt> (note that MySQL uses # <tt>:charset</tt> while PostgreSQL uses <tt>:encoding</tt>). # @@ -665,34 +666,33 @@ module ActiveRecord SQL end + # Returns true if table exists. + # If the schema is not specified as part of +name+ then it will only find tables within + # the current schema search path (regardless of permissions to access tables in other schemas) def table_exists?(name) schema, table = extract_schema_and_table(name.to_s) + return false unless table - binds = [[nil, table.gsub(/(^"|"$)/,'')]] + binds = [[nil, table]] binds << [nil, schema] if schema exec_query(<<-SQL, 'SCHEMA', binds).rows.first[0].to_i > 0 - SELECT COUNT(*) - FROM pg_tables - WHERE tablename = $1 - #{schema ? "AND schemaname = $2" : ''} + SELECT COUNT(*) + FROM pg_class c + LEFT JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE c.relkind in ('v','r') + AND c.relname = $1 + AND n.nspname = #{schema ? '$2' : 'ANY (current_schemas(false))'} SQL end - # Extracts the table and schema name from +name+ - def extract_schema_and_table(name) - schema, table = name.split('.', 2) - - unless table # A table was provided without a schema - table = schema - schema = nil - end - - if name =~ /^"/ # Handle quoted table names - table = name - schema = nil - end - [schema, table] + # Returns true if schema exists. + def schema_exists?(name) + exec_query(<<-SQL, 'SCHEMA', [[nil, name]]).rows.first[0].to_i > 0 + SELECT COUNT(*) + FROM pg_namespace + WHERE nspname = $1 + SQL end # Returns an array of indexes for the given table. @@ -742,6 +742,11 @@ module ActiveRecord query('select current_database()')[0][0] end + # Returns the current schema name. + def current_schema + query('SELECT current_schema', 'SCHEMA')[0][0] + end + # Returns the current database encoding format. def encoding query(<<-end_sql)[0][0] @@ -805,7 +810,7 @@ module ActiveRecord end if pk && sequence - quoted_sequence = quote_column_name(sequence) + quoted_sequence = quote_table_name(sequence) select_value <<-end_sql, 'Reset sequence' SELECT setval('#{quoted_sequence}', (SELECT COALESCE(MAX(#{quote_column_name pk})+(SELECT increment_by FROM #{quoted_sequence}), (SELECT min_value FROM #{quoted_sequence})) FROM #{quote_table_name(table)}), false) @@ -818,18 +823,25 @@ module ActiveRecord # First try looking for a sequence with a dependency on the # given table's primary key. result = exec_query(<<-end_sql, 'SCHEMA').rows.first - SELECT attr.attname, seq.relname + SELECT attr.attname, ns.nspname, seq.relname FROM pg_class seq INNER JOIN pg_depend dep ON seq.oid = dep.objid INNER JOIN pg_attribute attr ON attr.attrelid = dep.refobjid AND attr.attnum = dep.refobjsubid INNER JOIN pg_constraint cons ON attr.attrelid = cons.conrelid AND attr.attnum = cons.conkey[1] + INNER JOIN pg_namespace ns ON seq.relnamespace = ns.oid WHERE seq.relkind = 'S' AND cons.contype = 'p' AND dep.refobjid = '#{quote_table_name(table)}'::regclass end_sql # [primary_key, sequence] - [result.first, result.last] + if result.second == 'public' then + sequence = result.last + else + sequence = result.second+'.'+result.last + end + + [result.first, sequence] rescue nil end @@ -928,7 +940,7 @@ module ActiveRecord # Construct a clean list of column names from the ORDER BY clause, removing # any ASC/DESC modifiers - order_columns = orders.collect { |s| s =~ /^(.+)\s+(ASC|DESC)\s*$/i ? $1 : s } + order_columns = orders.collect { |s| s.gsub(/\s+(ASC|DESC)\s*/i, '') } order_columns.delete_if { |c| c.blank? } order_columns = order_columns.zip((0...order_columns.size).to_a).map { |s,i| "#{s} AS alias_#{i}" } @@ -953,27 +965,27 @@ module ActiveRecord end private - def exec_no_cache(sql, binds) - @connection.async_exec(sql) - end - - def exec_cache(sql, binds) - unless @statements.key? sql - nextkey = "a#{@statements.length + 1}" - @connection.prepare nextkey, sql - @statements[sql] = nextkey + def exec_no_cache(sql, binds) + @connection.async_exec(sql) end - key = @statements[sql] + def exec_cache(sql, binds) + unless @statements.key? sql + nextkey = "a#{@statements.length + 1}" + @connection.prepare nextkey, sql + @statements[sql] = nextkey + end - # Clear the queue - @connection.get_last_result - @connection.send_query_prepared(key, binds.map { |col, val| - type_cast(val, col) - }) - @connection.block - @connection.get_last_result - end + key = @statements[sql] + + # Clear the queue + @connection.get_last_result + @connection.send_query_prepared(key, binds.map { |col, val| + type_cast(val, col) + }) + @connection.block + @connection.get_last_result + end # The internal PostgreSQL identifier of the money data type. MONEY_COLUMN_TYPE_OID = 790 #:nodoc: @@ -1073,9 +1085,29 @@ module ActiveRecord end end - def table_definition - TableDefinition.new(self) - end + # Returns an array of <tt>[schema_name, table_name]</tt> extracted from +name+. + # +schema_name+ is nil if not specified in +name+. + # +schema_name+ and +table_name+ exclude surrounding quotes (regardless of whether provided in +name+) + # +name+ supports the range of schema/table references understood by PostgreSQL, for example: + # + # * <tt>table_name</tt> + # * <tt>"table.name"</tt> + # * <tt>schema_name.table_name</tt> + # * <tt>schema_name."table.name"</tt> + # * <tt>"schema.name"."table name"</tt> + def extract_schema_and_table(name) + table, schema = name.scan(/[^".\s]+|"[^"]*"/)[0..1].collect{|m| m.gsub(/(^"|"$)/,'') }.reverse + [schema, table] + end + + def extract_table_ref_from_insert_sql(sql) + sql[/into\s+([^\(]*).*values\s*\(/i] + $1.strip if $1 + end + + def table_definition + TableDefinition.new(self) + 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 3c6f52e0fa..724b2e6d9c 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb @@ -102,6 +102,10 @@ module ActiveRecord # Clears the prepared statements cache. def clear_cache! + @statements.values.map { |hash| hash[:stmt] }.each { |stmt| + stmt.close unless stmt.closed? + } + @statements.clear end diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb index 7839f03848..4d387565d9 100644 --- a/activerecord/lib/active_record/counter_cache.rb +++ b/activerecord/lib/active_record/counter_cache.rb @@ -2,7 +2,7 @@ module ActiveRecord # = Active Record Counter Cache module CounterCache # Resets one or more counter caches to their correct value using an SQL - # count query. This is useful when adding new counter caches, or if the + # count query. This is useful when adding new counter caches, or if the # counter has been corrupted or modified directly by SQL. # # ==== Parameters diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 1f31722a7c..819743a361 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -12,7 +12,6 @@ require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/object/blank' require 'active_support/core_ext/logger' require 'active_support/ordered_hash' -require 'active_support/core_ext/module/deprecation' require 'active_record/fixtures/file' if defined? ActiveRecord @@ -54,14 +53,14 @@ class FixturesFileNotFound < StandardError; end # name: Google # url: http://www.google.com # -# This YAML fixture file includes two fixtures. Each YAML fixture (ie. record) is given a name and is followed by an -# indented list of key/value pairs in the "key: value" format. Records are separated by a blank line for your viewing +# This YAML fixture file includes two fixtures. Each YAML fixture (ie. record) is given a name and is followed by an +# indented list of key/value pairs in the "key: value" format. Records are separated by a blank line for your viewing # pleasure. # # Note that YAML fixtures are unordered. If you want ordered fixtures, use the omap YAML type. # See http://yaml.org/type/omap.html -# for the specification. You will need ordered fixtures when you have foreign key constraints on keys in the same table. -# This is commonly needed for tree structures. Example: +# for the specification. You will need ordered fixtures when you have foreign key constraints on keys in the same table. +# This is commonly needed for tree structures. Example: # # --- !omap # - parent: @@ -75,7 +74,7 @@ class FixturesFileNotFound < StandardError; end # # = Using fixtures in testcases # -# Since fixtures are a testing construct, we use them in our unit and functional tests. There are two ways to use the +# Since fixtures are a testing construct, we use them in our unit and functional tests. There are two ways to use the # fixtures, but first let's take a look at a sample unit test: # # require 'test_helper' @@ -393,9 +392,6 @@ class FixturesFileNotFound < StandardError; end # # Any fixture labeled "DEFAULTS" is safely ignored. -Fixture = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('Fixture', 'ActiveRecord::Fixture') -Fixtures = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('Fixtures', 'ActiveRecord::Fixtures') - module ActiveRecord class Fixtures MAX_ID = 2 ** 30 - 1 @@ -558,7 +554,7 @@ module ActiveRecord fixtures.size end - # Return a hash of rows to be inserted. The key is the table, the value is + # Return a hash of rows to be inserted. The key is the table, the value is # a list of rows to insert to that table. def table_rows now = ActiveRecord::Base.default_timezone == :utc ? Time.now.utc : Time.now diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index 3afa257a76..6cfce6e573 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -73,7 +73,7 @@ module ActiveRecord # <tt>locking_enabled?</tt> at this point as # <tt>@attributes</tt> may not have been initialized yet. - if lock_optimistically && result.include?(self.class.locking_column) + if result.key?(self.class.locking_column) && lock_optimistically result[self.class.locking_column] ||= 0 end diff --git a/activerecord/lib/active_record/locking/pessimistic.rb b/activerecord/lib/active_record/locking/pessimistic.rb index 4c4c1bf5a1..66994e4797 100644 --- a/activerecord/lib/active_record/locking/pessimistic.rb +++ b/activerecord/lib/active_record/locking/pessimistic.rb @@ -14,7 +14,7 @@ module ActiveRecord # Account.transaction do # # select * from accounts where name = 'shugo' limit 1 for update # shugo = Account.where("name = 'shugo'").lock(true).first - # yuko = Account.where("name = 'shugo'").lock(true).first + # yuko = Account.where("name = 'yuko'").lock(true).first # shugo.balance -= 100 # shugo.save! # yuko.balance += 100 diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 640111096d..c459846264 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -53,7 +53,7 @@ module ActiveRecord # # This migration will add a boolean flag to the accounts table and remove it # if you're backing out of the migration. It shows how all migrations have - # two class methods +up+ and +down+ that describes the transformations + # two methods +up+ and +down+ that describes the transformations # required to implement or remove the migration. These methods can consist # of both the migration specific methods like add_column and remove_column, # but may also contain regular Ruby code for generating data needed for the @@ -66,9 +66,9 @@ module ActiveRecord # create_table :system_settings do |t| # t.string :name # t.string :label - # t.text :value + # t.text :value # t.string :type - # t.integer :position + # t.integer :position # end # # SystemSetting.create :name => "notice", @@ -116,8 +116,10 @@ module ActiveRecord # with the name of the column. Other options include # <tt>:name</tt> and <tt>:unique</tt> (e.g. # <tt>{ :name => "users_name_index", :unique => true }</tt>). - # * <tt>remove_index(table_name, index_name)</tt>: Removes the index specified - # by +index_name+. + # * <tt>remove_index(table_name, :column => column_name)</tt>: Removes the index + # specified by +column_name+. + # * <tt>remove_index(table_name, :name => index_name)</tt>: Removes the index + # specified by +index_name+. # # == Irreversible transformations # @@ -179,7 +181,7 @@ module ActiveRecord # # class RemoveEmptyTags < ActiveRecord::Migration # def up - # Tag.find(:all).each { |tag| tag.destroy if tag.pages.empty? } + # Tag.all.each { |tag| tag.destroy if tag.pages.empty? } # end # # def down @@ -225,7 +227,7 @@ module ActiveRecord # def up # add_column :people, :salary, :integer # Person.reset_column_information - # Person.find(:all).each do |p| + # Person.all.each do |p| # p.update_attribute :salary, SalaryCalculator.compute(p) # end # end @@ -245,7 +247,7 @@ module ActiveRecord # def up # ... # say_with_time "Updating salaries..." do - # Person.find(:all).each do |p| + # Person.all.each do |p| # p.update_attribute :salary, SalaryCalculator.compute(p) # end # end diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb index c9d57ce812..2eeff7e36f 100644 --- a/activerecord/lib/active_record/migration/command_recorder.rb +++ b/activerecord/lib/active_record/migration/command_recorder.rb @@ -1,12 +1,12 @@ module ActiveRecord class Migration - # ActiveRecord::Migration::CommandRecorder records commands done during - # a migration and knows how to reverse those commands. The CommandRecorder + # <tt>ActiveRecord::Migration::CommandRecorder</tt> records commands done during + # a migration and knows how to reverse those commands. The CommandRecorder # knows how to invert the following commands: # # * add_column # * add_index - # * add_timestamp + # * add_timestamps # * create_table # * remove_timestamps # * rename_column @@ -20,21 +20,21 @@ module ActiveRecord @delegate = delegate end - # record +command+. +command+ should be a method name and arguments. + # record +command+. +command+ should be a method name and arguments. # For example: # - # recorder.record(:method_name, [:arg1, arg2]) + # recorder.record(:method_name, [:arg1, :arg2]) def record(*command) @commands << command end # Returns a list that represents commands that are the inverse of the - # commands stored in +commands+. For example: + # commands stored in +commands+. For example: # # recorder.record(:rename_table, [:old, :new]) # recorder.inverse # => [:rename_table, [:new, :old]] # - # This method will raise an IrreversibleMigration exception if it cannot + # This method will raise an +IrreversibleMigration+ exception if it cannot # invert the +commands+. def inverse @commands.reverse.map { |name, args| @@ -48,11 +48,11 @@ module ActiveRecord super || delegate.respond_to?(*args) end - [:create_table, :rename_table, :add_column, :remove_column, :rename_index, :rename_column, :add_index, :remove_index, :add_timestamps, :remove_timestamps, :change_column, :change_column_default].each do |method| + [:create_table, :change_table, :rename_table, :add_column, :remove_column, :rename_index, :rename_column, :add_index, :remove_index, :add_timestamps, :remove_timestamps, :change_column, :change_column_default].each do |method| class_eval <<-EOV, __FILE__, __LINE__ + 1 - def #{method}(*args) - record(:"#{method}", args) - end + def #{method}(*args) # def create_table(*args) + record(:"#{method}", args) # record(:create_table, args) + end # end EOV end @@ -79,8 +79,10 @@ module ActiveRecord end def invert_add_index(args) - table, columns, _ = *args - [:remove_index, [table, {:column => columns}]] + table, columns, options = *args + index_name = options.try(:[], :name) + options_hash = index_name ? {:name => index_name} : {:column => columns} + [:remove_index, [table, options_hash]] end def invert_remove_timestamps(args) diff --git a/activerecord/lib/active_record/named_scope.rb b/activerecord/lib/active_record/named_scope.rb index 588f52be44..0313abe456 100644 --- a/activerecord/lib/active_record/named_scope.rb +++ b/activerecord/lib/active_record/named_scope.rb @@ -17,7 +17,7 @@ module ActiveRecord # posts.each {|p| puts p.name } # Fires "select * from posts" and loads post objects # # fruits = Fruit.scoped - # fruits = fruits.where(:colour => 'red') if options[:red_only] + # fruits = fruits.where(:color => 'red') if options[:red_only] # fruits = fruits.limit(10) if limited? # # Anonymous \scopes tend to be useful when procedurally generating complex @@ -40,6 +40,25 @@ module ActiveRecord end end + ## + # Collects attributes from scopes that should be applied when creating + # an AR instance for the particular class this is called on. + def scope_attributes # :nodoc: + if current_scope + current_scope.scope_for_create + else + scope = relation.clone + scope.default_scoped = true + scope.scope_for_create + end + end + + ## + # Are there default attributes associated with this scope? + def scope_attributes? # :nodoc: + current_scope || default_scopes.any? + end + # Adds a class method for retrieving and querying objects. A \scope represents a narrowing of a database query, # such as <tt>where(:color => :red).select('shirts.*').includes(:washing_instructions)</tt>. # diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index 08b27b6a8e..53617059d0 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -277,14 +277,14 @@ module ActiveRecord type = (reflection.collection? ? :collection : :one_to_one) # def pirate_attributes=(attributes) - # assign_nested_attributes_for_one_to_one_association(:pirate, attributes) + # assign_nested_attributes_for_one_to_one_association(:pirate, attributes, mass_assignment_options) # end class_eval <<-eoruby, __FILE__, __LINE__ + 1 if method_defined?(:#{association_name}_attributes=) remove_method(:#{association_name}_attributes=) end def #{association_name}_attributes=(attributes) - assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes) + assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes, mass_assignment_options) end eoruby else @@ -319,21 +319,21 @@ module ActiveRecord # If the given attributes include a matching <tt>:id</tt> attribute, or # update_only is true, and a <tt>:_destroy</tt> key set to a truthy value, # then the existing record will be marked for destruction. - def assign_nested_attributes_for_one_to_one_association(association_name, attributes) + def assign_nested_attributes_for_one_to_one_association(association_name, attributes, assignment_opts = {}) options = self.nested_attributes_options[association_name] attributes = attributes.with_indifferent_access if (options[:update_only] || !attributes['id'].blank?) && (record = send(association_name)) && (options[:update_only] || record.id.to_s == attributes['id'].to_s) - assign_to_or_mark_for_destruction(record, attributes, options[:allow_destroy]) unless call_reject_if(association_name, attributes) + assign_to_or_mark_for_destruction(record, attributes, options[:allow_destroy], assignment_opts) unless call_reject_if(association_name, attributes) - elsif attributes['id'].present? + elsif attributes['id'].present? && !assignment_opts[:without_protection] raise_nested_attributes_record_not_found(association_name, attributes['id']) elsif !reject_new_record?(association_name, attributes) method = "build_#{association_name}" if respond_to?(method) - send(method, attributes.except(*UNASSIGNABLE_KEYS)) + send(method, attributes.except(*unassignable_keys(assignment_opts)), assignment_opts) else raise ArgumentError, "Cannot build association #{association_name}. Are you trying to build a polymorphic one-to-one association?" end @@ -367,7 +367,7 @@ module ActiveRecord # { :name => 'John' }, # { :id => '2', :_destroy => true } # ]) - def assign_nested_attributes_for_collection_association(association_name, attributes_collection) + def assign_nested_attributes_for_collection_association(association_name, attributes_collection, assignment_opts = {}) options = self.nested_attributes_options[association_name] unless attributes_collection.is_a?(Hash) || attributes_collection.is_a?(Array) @@ -401,7 +401,7 @@ module ActiveRecord if attributes['id'].blank? unless reject_new_record?(association_name, attributes) - association.build(attributes.except(*UNASSIGNABLE_KEYS)) + association.build(attributes.except(*unassignable_keys(assignment_opts)), assignment_opts) end elsif existing_record = existing_records.detect { |record| record.id.to_s == attributes['id'].to_s } unless association.loaded? || call_reject_if(association_name, attributes) @@ -418,8 +418,10 @@ module ActiveRecord end if !call_reject_if(association_name, attributes) - assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy]) + assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy], assignment_opts) end + elsif assignment_opts[:without_protection] + association.build(attributes.except(*unassignable_keys(assignment_opts)), assignment_opts) else raise_nested_attributes_record_not_found(association_name, attributes['id']) end @@ -428,8 +430,8 @@ module ActiveRecord # Updates a record with the +attributes+ or marks it for destruction if # +allow_destroy+ is +true+ and has_destroy_flag? returns +true+. - def assign_to_or_mark_for_destruction(record, attributes, allow_destroy) - record.attributes = attributes.except(*UNASSIGNABLE_KEYS) + def assign_to_or_mark_for_destruction(record, attributes, allow_destroy, assignment_opts) + record.assign_attributes(attributes.except(*unassignable_keys(assignment_opts)), assignment_opts) record.mark_for_destruction if has_destroy_flag?(attributes) && allow_destroy end @@ -458,5 +460,9 @@ module ActiveRecord def raise_nested_attributes_record_not_found(association_name, record_id) raise RecordNotFound, "Couldn't find #{self.class.reflect_on_association(association_name).klass.name} with ID=#{record_id} for #{self.class.name} with ID=#{id}" end + + def unassignable_keys(assignment_opts) + assignment_opts[:without_protection] ? UNASSIGNABLE_KEYS - %w[id] : UNASSIGNABLE_KEYS + end end end diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index b9041f44d8..f425118f59 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -33,7 +33,11 @@ module ActiveRecord # +save+ returns +false+. See ActiveRecord::Callbacks for further # details. def save(*) - create_or_update + begin + create_or_update + rescue ActiveRecord::RecordInvalid + false + end end # Saves the model. @@ -133,6 +137,8 @@ module ActiveRecord # * Callbacks are skipped. # * updated_at/updated_on column is not updated if that column is available. # + # Raises an +ActiveRecordError+ when called on new objects, or when the +name+ + # attribute is marked as readonly. def update_column(name, value) name = name.to_s raise ActiveRecordError, "#{name} is marked as readonly" if self.class.readonly_attributes.include?(name) @@ -273,7 +279,7 @@ module ActiveRecord @changed_attributes.except!(*changes.keys) primary_key = self.class.primary_key - self.class.update_all(changes, { primary_key => self[primary_key] }) == 1 + self.class.unscoped.update_all(changes, { primary_key => self[primary_key] }) == 1 end end @@ -313,9 +319,7 @@ module ActiveRecord # that a new instance, or one populated from a passed-in Hash, still has all the attributes # that instances loaded from the database would. def attributes_from_column_definition - Hash[self.class.columns.map do |column| - [column.name, column.default] - end] + self.class.column_defaults.dup end end end diff --git a/activerecord/lib/active_record/query_cache.rb b/activerecord/lib/active_record/query_cache.rb index 4e61671473..e485901440 100644 --- a/activerecord/lib/active_record/query_cache.rb +++ b/activerecord/lib/active_record/query_cache.rb @@ -33,6 +33,14 @@ module ActiveRecord @target = target end + def method_missing(method_sym, *arguments, &block) + @target.send(method_sym, *arguments, &block) + end + + def respond_to?(method_sym, include_private = false) + super || @target.respond_to?(method_sym) + end + def each(&block) @target.each(&block) end diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index bae2ded244..47133e77e8 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -29,8 +29,8 @@ module ActiveRecord # When loading console, force ActiveRecord::Base to be loaded # to avoid cross references when loading a constant for the # first time. Also, make it output to STDERR. - console do |sandbox| - require "active_record/railties/console_sandbox" if sandbox + console do |app| + require "active_record/railties/console_sandbox" if app.sandbox? ActiveRecord::Base.logger = Logger.new(STDERR) end diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 2c7298e220..399f3c5d03 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -369,7 +369,7 @@ db_namespace = namespace :db do ENV['PGPASSWORD'] = abcs[Rails.env]['password'].to_s if abcs[Rails.env]['password'] search_path = abcs[Rails.env]['schema_search_path'] unless search_path.blank? - search_path = search_path.split(",").map{|search_path| "--schema=#{search_path.strip}" }.join(" ") + search_path = search_path.split(",").map{|search_path_part| "--schema=#{search_path_part.strip}" }.join(" ") end `pg_dump -i -U "#{abcs[Rails.env]['username']}" -s -x -O -f db/#{Rails.env}_structure.sql #{search_path} #{abcs[Rails.env]['database']}` raise 'Error dumping database' if $?.exitstatus == 1 @@ -377,8 +377,7 @@ db_namespace = namespace :db do dbfile = abcs[Rails.env]['database'] || abcs[Rails.env]['dbfile'] `sqlite3 #{dbfile} .schema > db/#{Rails.env}_structure.sql` when 'sqlserver' - `scptxfr /s #{abcs[Rails.env]['host']} /d #{abcs[Rails.env]['database']} /I /f db\\#{Rails.env}_structure.sql /q /A /r` - `scptxfr /s #{abcs[Rails.env]['host']} /d #{abcs[Rails.env]['database']} /I /F db\ /q /A /r` + `smoscript -s #{abcs[Rails.env]['host']} -d #{abcs[Rails.env]['database']} -u #{abcs[Rails.env]['username']} -p #{abcs[Rails.env]['password']} -f db\\#{Rails.env}_structure.sql -A -U` when "firebird" set_firebird_env(abcs[Rails.env]) db_string = firebird_db_string(abcs[Rails.env]) @@ -423,7 +422,7 @@ db_namespace = namespace :db do dbfile = abcs['test']['database'] || abcs['test']['dbfile'] `sqlite3 #{dbfile} < #{Rails.root}/db/#{Rails.env}_structure.sql` when 'sqlserver' - `osql -E -S #{abcs['test']['host']} -d #{abcs['test']['database']} -i db\\#{Rails.env}_structure.sql` + `sqlcmd -S #{abcs['test']['host']} -d #{abcs['test']['database']} -U #{abcs['test']['username']} -P #{abcs['test']['password']} -i db\\#{Rails.env}_structure.sql` when 'oci', 'oracle' ActiveRecord::Base.establish_connection(:test) IO.readlines("#{Rails.root}/db/#{Rails.env}_structure.sql").join.split(";\n\n").each do |ddl| @@ -453,9 +452,11 @@ db_namespace = namespace :db do dbfile = abcs['test']['database'] || abcs['test']['dbfile'] File.delete(dbfile) if File.exist?(dbfile) when 'sqlserver' - dropfkscript = "#{abcs['test']['host']}.#{abcs['test']['database']}.DP1".gsub(/\\/,'-') - `osql -E -S #{abcs['test']['host']} -d #{abcs['test']['database']} -i db\\#{dropfkscript}` - `osql -E -S #{abcs['test']['host']} -d #{abcs['test']['database']} -i db\\#{Rails.env}_structure.sql` + test = abcs.deep_dup['test'] + test_database = test['database'] + test['database'] = 'master' + ActiveRecord::Base.establish_connection(test) + ActiveRecord::Base.connection.recreate_database!(test_database) when "oci", "oracle" ActiveRecord::Base.establish_connection(:test) ActiveRecord::Base.connection.structure_drop.split(";\n\n").each do |ddl| @@ -481,8 +482,7 @@ db_namespace = namespace :db do # desc "Creates a sessions migration for use with ActiveRecord::SessionStore" task :create => :environment do raise 'Task unavailable to this database (no migration support)' unless ActiveRecord::Base.connection.supports_migrations? - require 'rails/generators' - Rails::Generators.configure! + Rails.application.load_generators require 'rails/generators/rails/session_migration/session_migration_generator' Rails::Generators::SessionMigrationGenerator.start [ ENV['MIGRATION'] || 'add_sessions_table' ] end @@ -499,7 +499,7 @@ namespace :railties do # desc "Copies missing migrations from Railties (e.g. plugins, engines). You can specify Railties to use with FROM=railtie1,railtie2" task :migrations => :'db:load_config' do to_load = ENV['FROM'].blank? ? :all : ENV['FROM'].split(",").map {|n| n.strip } - railties = {} + railties = ActiveSupport::OrderedHash.new Rails.application.railties.all do |railtie| next unless to_load == :all || to_load.include?(railtie.railtie_name) diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index b1bb1b0f7f..a2324039cf 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -1,5 +1,4 @@ require 'active_support/core_ext/class/attribute' -require 'active_support/core_ext/module/deprecation' require 'active_support/core_ext/object/inclusion' module ActiveRecord @@ -81,12 +80,6 @@ module ActiveRecord # Abstract base class for AggregateReflection and AssociationReflection. Objects of # AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods. class MacroReflection - attr_reader :active_record - - def initialize(macro, name, options, active_record) - @macro, @name, @options, @active_record = macro, name, options, active_record - end - # Returns the name of the macro. # # <tt>composed_of :balance, :class_name => 'Money'</tt> returns <tt>:balance</tt> @@ -105,6 +98,19 @@ module ActiveRecord # <tt>has_many :clients</tt> returns +{}+ attr_reader :options + attr_reader :active_record + + attr_reader :plural_name # :nodoc: + + def initialize(macro, name, options, active_record) + @macro = macro + @name = name + @options = options + @active_record = active_record + @plural_name = active_record.pluralize_table_names ? + name.to_s.pluralize : name.to_s + end + # Returns the class for the macro. # # <tt>composed_of :balance, :class_name => 'Money'</tt> returns the Money class @@ -124,7 +130,11 @@ module ActiveRecord # Returns +true+ if +self+ and +other_aggregation+ have the same +name+ attribute, +active_record+ attribute, # and +other_aggregation+ has an options hash assigned to it. def ==(other_aggregation) - other_aggregation.kind_of?(self.class) && name == other_aggregation.name && other_aggregation.options && active_record == other_aggregation.active_record + super || + other_aggregation.kind_of?(self.class) && + name == other_aggregation.name && + other_aggregation.options && + active_record == other_aggregation.active_record end def sanitized_conditions #:nodoc: @@ -169,25 +179,8 @@ module ActiveRecord # Returns a new, unsaved instance of the associated class. +options+ will # be passed to the class's constructor. - def build_association(*options) - klass.new(*options) - end - - # Creates a new instance of the associated class, and immediately saves it - # with ActiveRecord::Base#save. +options+ will be passed to the class's - # creation method. Returns the newly created object. - def create_association(*options) - klass.create(*options) - end - - # Creates a new instance of the associated class, and immediately saves it - # with ActiveRecord::Base#save!. +options+ will be passed to the class's - # creation method. If the created record doesn't pass validations, then an - # exception will be raised. - # - # Returns the newly created object. - def create_association!(*options) - klass.create!(*options) + def build_association(*options, &block) + klass.new(*options, &block) end def table_name @@ -202,11 +195,6 @@ module ActiveRecord @foreign_key ||= options[:foreign_key] || derive_foreign_key end - def primary_key_name - foreign_key - end - deprecate :primary_key_name => :foreign_key - def foreign_type @foreign_type ||= options[:foreign_type] || "#{name}_type" end @@ -379,7 +367,7 @@ module ActiveRecord delegate :foreign_key, :foreign_type, :association_foreign_key, :active_record_primary_key, :type, :to => :source_reflection - # Gets the source of the through reflection. It checks both a singularized + # Gets the source of the through reflection. It checks both a singularized # and pluralized form for <tt>:belongs_to</tt> or <tt>:has_many</tt>. # # class Post < ActiveRecord::Base diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index ae9afad48a..9cd146b857 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -6,13 +6,13 @@ module ActiveRecord JoinOperation = Struct.new(:relation, :join_class, :on) ASSOCIATION_METHODS = [:includes, :eager_load, :preload] MULTI_VALUE_METHODS = [:select, :group, :order, :joins, :where, :having, :bind] - SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :create_with, :from, :reorder] + SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :from, :reorder, :reverse_order] include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches # These are explicitly delegated to improve performance (avoids method_missing) delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to => :to_a - delegate :table_name, :quoted_table_name, :primary_key, :quoted_primary_key, :to => :klass + delegate :table_name, :quoted_table_name, :primary_key, :quoted_primary_key, :connection, :column_hash,:to => :klass attr_reader :table, :klass, :loaded attr_accessor :extensions, :default_scoped @@ -29,6 +29,7 @@ module ActiveRecord SINGLE_VALUE_METHODS.each {|v| instance_variable_set(:"@#{v}_value", nil)} (ASSOCIATION_METHODS + MULTI_VALUE_METHODS).each {|v| instance_variable_set(:"@#{v}_values", [])} @extensions = [] + @create_with_value = {} end def insert(values) @@ -102,24 +103,30 @@ module ActiveRecord def to_a return @records if loaded? - @records = if @readonly_value.nil? && !@klass.locking_enabled? - eager_loading? ? find_with_associations : @klass.find_by_sql(arel.to_sql, @bind_values) - else - IdentityMap.without do + default_scoped = with_default_scope + + if default_scoped.equal?(self) + @records = if @readonly_value.nil? && !@klass.locking_enabled? eager_loading? ? find_with_associations : @klass.find_by_sql(arel.to_sql, @bind_values) + else + IdentityMap.without do + eager_loading? ? find_with_associations : @klass.find_by_sql(arel.to_sql, @bind_values) + end end - end - preload = @preload_values - preload += @includes_values unless eager_loading? - preload.each do |associations| - ActiveRecord::Associations::Preloader.new(@records, associations).run - end + preload = @preload_values + preload += @includes_values unless eager_loading? + preload.each do |associations| + ActiveRecord::Associations::Preloader.new(@records, associations).run + end - # @readonly_value is true only if set explicitly. @implicit_readonly is true if there - # are JOINS and no explicit SELECT. - readonly = @readonly_value.nil? ? @implicit_readonly : @readonly_value - @records.each { |record| record.readonly! } if readonly + # @readonly_value is true only if set explicitly. @implicit_readonly is true if there + # are JOINS and no explicit SELECT. + readonly = @readonly_value.nil? ? @implicit_readonly : @readonly_value + @records.each { |record| record.readonly! } if readonly + else + @records = default_scoped.to_a + end @loaded = true @records @@ -287,7 +294,7 @@ module ActiveRecord end # Destroy an object (or multiple objects) that has the given id, the object is instantiated first, - # therefore all callbacks and filters are fired off before the object is deleted. This method is + # therefore all callbacks and filters are fired off before the object is deleted. This method is # less efficient than ActiveRecord#delete but allows cleanup methods and other actions to be run. # # This essentially finds the object (or multiple objects) with the given id, creates a new object @@ -316,7 +323,7 @@ module ActiveRecord # Deletes the records matching +conditions+ without instantiating the records first, and hence not # calling the +destroy+ method nor invoking callbacks. This is a single SQL DELETE statement that # goes straight to the database, much more efficient than +destroy_all+. Be careful with relations - # though, in particular <tt>:dependent</tt> rules defined on associations are not honored. Returns + # though, in particular <tt>:dependent</tt> rules defined on associations are not honored. Returns # the number of rows affected. # # ==== Parameters @@ -397,7 +404,7 @@ module ActiveRecord end def scope_for_create - @scope_for_create ||= where_values_hash.merge(@create_with_value || {}) + @scope_for_create ||= where_values_hash.merge(create_with_value) end def eager_loading? @@ -418,9 +425,10 @@ module ActiveRecord end def with_default_scope #:nodoc: - if default_scoped? - default_scope = @klass.send(:build_default_scope) - default_scope ? default_scope.merge(self) : self + if default_scoped? && default_scope = klass.send(:build_default_scope) + default_scope = default_scope.merge(self) + default_scope.default_scoped = false + default_scope else self end diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 0fcae92d51..0ac821b2d7 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -66,7 +66,7 @@ module ActiveRecord calculate(:average, column_name, options) end - # Calculates the minimum value on a given column. The value is returned + # Calculates the minimum value on a given column. The value is returned # with the same data type of the column, or +nil+ if there's no row. See # +calculate+ for examples with options. # @@ -89,11 +89,15 @@ module ActiveRecord # +calculate+ for examples with options. # # Person.sum('age') # => 4562 - def sum(column_name, options = {}) - calculate(:sum, column_name, options) + def sum(*args) + if block_given? + self.to_a.sum(*args) {|*block_args| yield(*block_args)} + else + calculate(:sum, *args) + end end - # This calculates aggregate values in the given column. Methods for count, sum, average, + # This calculates aggregate values in the given column. Methods for count, sum, average, # minimum, and maximum have been added as shortcuts. Options such as <tt>:conditions</tt>, # <tt>:order</tt>, <tt>:group</tt>, <tt>:having</tt>, and <tt>:joins</tt> can be passed to customize the query. # @@ -101,7 +105,7 @@ module ActiveRecord # * Single aggregate value: The single value is type cast to Fixnum for COUNT, Float # for AVG, and the given column's type for everything else. # * Grouped values: This returns an ordered hash of the values and groups them by the - # <tt>:group</tt> option. It takes either a column name, or the name of a belongs_to association. + # <tt>:group</tt> option. It takes either a column name, or the name of a belongs_to association. # # values = Person.maximum(:age, :group => 'last_name') # puts values["Drake"] @@ -119,7 +123,7 @@ module ActiveRecord # Options: # * <tt>:conditions</tt> - An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. # See conditions in the intro to ActiveRecord::Base. - # * <tt>:include</tt>: Eager loading, see Associations for details. Since calculations don't load anything, + # * <tt>:include</tt>: Eager loading, see Associations for details. Since calculations don't load anything, # the purpose of this is to access fields on joined tables in your conditions, order, or group clauses. # * <tt>:joins</tt> - An SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id". # (Rarely needed). @@ -146,10 +150,16 @@ module ActiveRecord if options.except(:distinct).present? apply_finder_options(options.except(:distinct)).calculate(operation, column_name, :distinct => options[:distinct]) else - if eager_loading? || (includes_values.present? && references_eager_loaded_tables?) - construct_relation_for_association_calculations.calculate(operation, column_name, options) + relation = with_default_scope + + if relation.equal?(self) + if eager_loading? || (includes_values.present? && references_eager_loaded_tables?) + construct_relation_for_association_calculations.calculate(operation, column_name, options) + else + perform_calculation(operation, column_name, options) + end else - perform_calculation(operation, column_name, options) + relation.calculate(operation, column_name, options) end end rescue ThrowResult diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 12d0a0a370..422d89060b 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -83,7 +83,7 @@ module ActiveRecord # # Example for find with a lock: Imagine two concurrent transactions: # each will read <tt>person.visits == 2</tt>, add 1 to it, and save, resulting - # in two saves of <tt>person.visits = 3</tt>. By locking the row, the second + # in two saves of <tt>person.visits = 3</tt>. By locking the row, the second # transaction has to wait until the first is finished; we get the # expected <tt>person.visits == 4</tt>. # diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 94aa999715..ec2e4d4dcd 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -9,7 +9,7 @@ module ActiveRecord :select_values, :group_values, :order_values, :joins_values, :where_values, :having_values, :bind_values, :limit_value, :offset_value, :lock_value, :readonly_value, :create_with_value, - :from_value, :reorder_value + :from_value, :reorder_value, :reverse_order_value def includes(*args) args.reject! {|a| a.blank? } @@ -137,7 +137,7 @@ module ActiveRecord def create_with(value) relation = clone - relation.create_with_value = value && (@create_with_value || {}).merge(value) + relation.create_with_value = value ? create_with_value.merge(value) : {} relation end @@ -158,13 +158,9 @@ module ActiveRecord end def reverse_order - order_clause = arel.order_clauses - - order = order_clause.empty? ? - "#{table_name}.#{primary_key} DESC" : - reverse_sql_order(order_clause).join(', ') - - except(:order).order(Arel.sql(order)) + relation = clone + relation.reverse_order_value = !relation.reverse_order_value + relation end def arel @@ -186,6 +182,7 @@ module ActiveRecord arel.group(*@group_values.uniq.reject{|g| g.blank?}) unless @group_values.empty? order = @reorder_value ? @reorder_value : @order_values + order = reverse_sql_order(order) if @reverse_order_value arel.order(*order.uniq.reject{|o| o.blank?}) unless order.empty? build_select(arel, @select_values.uniq) @@ -306,9 +303,20 @@ module ActiveRecord end def reverse_sql_order(order_query) - order_query.join(', ').split(',').collect do |s| - s.gsub!(/\sasc\Z/i, ' DESC') || s.gsub!(/\sdesc\Z/i, ' ASC') || s.concat(' DESC') - end + order_query = ["#{quoted_table_name}.#{quoted_primary_key} ASC"] if order_query.empty? + + order_query.map do |o| + case o + when Arel::Nodes::Ascending, Arel::Nodes::Descending + o.reverse + when String, Symbol + o.to_s.split(',').collect do |s| + s.gsub!(/\sasc\Z/i, ' DESC') || s.gsub!(/\sdesc\Z/i, ' ASC') || s.concat(' DESC') + end + else + o + end + end.flatten end def array_of_strings?(o) diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index 69706b5ead..ba882beca9 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -55,7 +55,7 @@ module ActiveRecord merged_relation.lock_value = r.lock_value unless merged_relation.lock_value - merged_relation = merged_relation.create_with(r.create_with_value) if r.create_with_value + merged_relation = merged_relation.create_with(r.create_with_value) unless r.create_with_value.empty? # Apply scope extension modules merged_relation.send :apply_modules, r.extensions diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb index 243012f88c..9ceab2eabc 100644 --- a/activerecord/lib/active_record/result.rb +++ b/activerecord/lib/active_record/result.rb @@ -1,7 +1,7 @@ module ActiveRecord ### # This class encapsulates a Result returned from calling +exec_query+ on any - # database connection adapter. For example: + # database connection adapter. For example: # # x = ActiveRecord::Base.connection.exec_query('SELECT * FROM foo') # x # => #<ActiveRecord::Result:0xdeadbeef> diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index a893c0ad85..19585f6214 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -106,7 +106,7 @@ HEADER spec = {} spec[:name] = column.name.inspect - # AR has an optimisation which handles zero-scale decimals as integers. This + # AR has an optimization which handles zero-scale decimals as integers. This # code ensures that the dumper still dumps the column as a decimal. spec[:type] = if column.type == :integer && [/^numeric/, /^decimal/].any? { |e| e.match(column.sql_type) } 'decimal' diff --git a/activerecord/lib/active_record/serialization.rb b/activerecord/lib/active_record/serialization.rb index be4354ce6a..e3185a9c5a 100644 --- a/activerecord/lib/active_record/serialization.rb +++ b/activerecord/lib/active_record/serialization.rb @@ -31,9 +31,6 @@ module ActiveRecord #:nodoc: def serializable_add_includes(options = {}) return unless 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) @@ -46,9 +43,8 @@ module ActiveRecord #:nodoc: end if records - association_options = include_has_options ? include_associations[association] : base_only_or_except - opts = options.merge(association_options) - yield(association, records, opts) + association_options = include_has_options ? include_associations[association] : {} + yield(association, records, association_options) end end diff --git a/activerecord/lib/active_record/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb index 8c4adf7116..f8e6cf958c 100644 --- a/activerecord/lib/active_record/serializers/xml_serializer.rb +++ b/activerecord/lib/active_record/serializers/xml_serializer.rb @@ -75,7 +75,7 @@ module ActiveRecord #:nodoc: # </firm> # # Additionally, the record being serialized will be passed to a Proc's second - # parameter. This allows for ad hoc additions to the resultant document that + # parameter. This allows for ad hoc additions to the resultant document that # incorporate the context of the record being serialized. And by leveraging the # closure created by a Proc, to_xml can be used to add elements that normally fall # outside of the scope of the model -- for example, generating and appending URLs diff --git a/activerecord/lib/active_record/session_store.rb b/activerecord/lib/active_record/session_store.rb index c3e976002e..929559c3ba 100644 --- a/activerecord/lib/active_record/session_store.rb +++ b/activerecord/lib/active_record/session_store.rb @@ -1,7 +1,7 @@ module ActiveRecord # = Active Record Session Store # - # A session store backed by an Active Record class. A default class is + # A session store backed by an Active Record class. A default class is # provided, but any object duck-typing to an Active Record Session class # with text +session_id+ and +data+ attributes is sufficient. # @@ -23,7 +23,7 @@ module ActiveRecord # ActiveRecord::SessionStore::Session.data_column_name = 'legacy_session_data' # # Note that setting the primary key to the +session_id+ frees you from - # having a separate +id+ column if you don't want it. However, you must + # having a separate +id+ column if you don't want it. However, you must # set <tt>session.model.id = session.session_id</tt> by hand! A before filter # on ApplicationController is a good place. # @@ -46,7 +46,7 @@ module ActiveRecord # save # destroy # - # The example SqlBypass class is a generic SQL session store. You may + # The example SqlBypass class is a generic SQL session store. You may # use it as a basis for high-performance database-specific stores. class SessionStore < ActionDispatch::Session::AbstractStore module ClassMethods # :nodoc: @@ -79,7 +79,7 @@ module ActiveRecord ## # :singleton-method: - # Customizable data column name. Defaults to 'data'. + # Customizable data column name. Defaults to 'data'. cattr_accessor :data_column_name self.data_column_name = 'data' @@ -161,12 +161,12 @@ module ActiveRecord end # A barebones session store which duck-types with the default session - # store but bypasses Active Record and issues SQL directly. This is + # store but bypasses Active Record and issues SQL directly. This is # an example session model class meant as a basis for your own classes. # # The database connection, table name, and session id and data columns - # are configurable class attributes. Marshaling and unmarshaling - # are implemented as class methods that you may override. By default, + # are configurable class attributes. Marshaling and unmarshaling + # are implemented as class methods that you may override. By default, # marshaling data is # # ActiveSupport::Base64.encode64(Marshal.dump(data)) @@ -176,7 +176,7 @@ module ActiveRecord # Marshal.load(ActiveSupport::Base64.decode64(data)) # # This marshaling behavior is intended to store the widest range of - # binary session data in a +text+ column. For higher performance, + # binary session data in a +text+ column. For higher performance, # store in a +blob+ column instead and forgo the Base64 encoding. class SqlBypass extend ClassMethods @@ -286,7 +286,7 @@ module ActiveRecord end end - # The class used for session storage. Defaults to + # The class used for session storage. Defaults to # ActiveRecord::SessionStore::Session cattr_accessor :session_class self.session_class = Session diff --git a/activerecord/lib/active_record/test_case.rb b/activerecord/lib/active_record/test_case.rb index 0d47eb3338..ffe9b08dce 100644 --- a/activerecord/lib/active_record/test_case.rb +++ b/activerecord/lib/active_record/test_case.rb @@ -13,7 +13,7 @@ module ActiveRecord ActiveRecord::IdentityMap.clear end - # Backport skip to Ruby 1.8. test/unit doesn't support it, so just + # Backport skip to Ruby 1.8. test/unit doesn't support it, so just # make it a noop. unless instance_methods.map(&:to_s).include?("skip") def skip(message) @@ -31,27 +31,30 @@ module ActiveRecord end def assert_sql(*patterns_to_match) - $queries_executed = [] + ActiveRecord::SQLCounter.log = [] yield - $queries_executed + ActiveRecord::SQLCounter.log ensure failed_patterns = [] patterns_to_match.each do |pattern| - failed_patterns << pattern unless $queries_executed.any?{ |sql| pattern === sql } + failed_patterns << pattern unless ActiveRecord::SQLCounter.log.any?{ |sql| pattern === sql } end - assert failed_patterns.empty?, "Query pattern(s) #{failed_patterns.map{ |p| p.inspect }.join(', ')} not found.#{$queries_executed.size == 0 ? '' : "\nQueries:\n#{$queries_executed.join("\n")}"}" + assert failed_patterns.empty?, "Query pattern(s) #{failed_patterns.map{ |p| p.inspect }.join(', ')} not found.#{ActiveRecord::SQLCounter.log.size == 0 ? '' : "\nQueries:\n#{ActiveRecord::SQLCounter.log.join("\n")}"}" end def assert_queries(num = 1) - $queries_executed = [] + ActiveRecord::SQLCounter.log = [] yield ensure - %w{ BEGIN COMMIT }.each { |x| $queries_executed.delete(x) } - assert_equal num, $queries_executed.size, "#{$queries_executed.size} instead of #{num} queries were executed.#{$queries_executed.size == 0 ? '' : "\nQueries:\n#{$queries_executed.join("\n")}"}" + assert_equal num, ActiveRecord::SQLCounter.log.size, "#{ActiveRecord::SQLCounter.log.size} instead of #{num} queries were executed.#{ActiveRecord::SQLCounter.log.size == 0 ? '' : "\nQueries:\n#{ActiveRecord::SQLCounter.log.join("\n")}"}" end def assert_no_queries(&block) + prev_ignored_sql = ActiveRecord::SQLCounter.ignored_sql + ActiveRecord::SQLCounter.ignored_sql = [] assert_queries(0, &block) + ensure + ActiveRecord::SQLCounter.ignored_sql = prev_ignored_sql end def with_kcode(kcode) diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index d363f36108..ae97a3f3ca 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -165,7 +165,7 @@ module ActiveRecord # writing, the only database that we're aware of that supports true nested # transactions, is MS-SQL. Because of this, Active Record emulates nested # transactions by using savepoints on MySQL and PostgreSQL. See - # http://dev.mysql.com/doc/refman/5.0/en/savepoints.html + # http://dev.mysql.com/doc/refman/5.0/en/savepoint.html # for more information about savepoints. # # === Callbacks diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index 59b6876135..4b075183c3 100644 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -1,7 +1,7 @@ module ActiveRecord # = Active Record RecordInvalid # - # Raised by <tt>save!</tt> and <tt>create!</tt> when the record is invalid. Use the + # Raised by <tt>save!</tt> and <tt>create!</tt> when the record is invalid. Use the # +record+ method to retrieve the record which did not validate. # # begin diff --git a/activerecord/lib/active_record/validations/associated.rb b/activerecord/lib/active_record/validations/associated.rb index 3a783aeb00..5df85304a2 100644 --- a/activerecord/lib/active_record/validations/associated.rb +++ b/activerecord/lib/active_record/validations/associated.rb @@ -37,10 +37,10 @@ module ActiveRecord # validation contexts by default (+nil+), other options are <tt>:create</tt> # and <tt>:update</tt>. # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should - # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The + # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The # method, proc or string should return or evaluate to a true or false value. # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should - # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The + # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The # method, proc or string should return or evaluate to a true or false value. def validates_associated(*attr_names) validates_with AssociatedValidator, _merge_attributes(attr_names) diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 4db4105389..484b1d369b 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -83,7 +83,7 @@ module ActiveRecord # validates_uniqueness_of :user_name, :scope => :account_id # end # - # Or even multiple scope parameters. For example, making sure that a teacher can only be on the schedule once + # Or even multiple scope parameters. For example, making sure that a teacher can only be on the schedule once # per semester for a particular class. # # class TeacherSchedule < ActiveRecord::Base @@ -105,7 +105,7 @@ module ActiveRecord # The method, proc or string should return or evaluate to a true or false value. # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should # not occur (e.g. <tt>:unless => :skip_validation</tt>, or - # <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The method, proc or string should + # <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The method, proc or string should # return or evaluate to a true or false value. # # === Concurrency and integrity diff --git a/activerecord/lib/active_record/version.rb b/activerecord/lib/active_record/version.rb index 43fe1cdee5..838aa8fb1e 100644 --- a/activerecord/lib/active_record/version.rb +++ b/activerecord/lib/active_record/version.rb @@ -1,9 +1,9 @@ module ActiveRecord module VERSION #:nodoc: MAJOR = 3 - MINOR = 1 + MINOR = 2 TINY = 0 - PRE = "rc1" + PRE = "beta" STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.') end diff --git a/activerecord/lib/rails/generators/active_record/session_migration/templates/migration.rb b/activerecord/lib/rails/generators/active_record/session_migration/templates/migration.rb index 8f0bf1ef0d..9ea3248513 100644 --- a/activerecord/lib/rails/generators/active_record/session_migration/templates/migration.rb +++ b/activerecord/lib/rails/generators/active_record/session_migration/templates/migration.rb @@ -1,5 +1,5 @@ class <%= migration_class_name %> < ActiveRecord::Migration - def up + def change create_table :<%= session_table_name %> do |t| t.string :session_id, :null => false t.text :data @@ -9,8 +9,4 @@ class <%= migration_class_name %> < ActiveRecord::Migration add_index :<%= session_table_name %>, :session_id add_index :<%= session_table_name %>, :updated_at end - - def down - drop_table :<%= session_table_name %> - end end diff --git a/activerecord/test/.gitignore b/activerecord/test/.gitignore new file mode 100644 index 0000000000..a0ec5967dd --- /dev/null +++ b/activerecord/test/.gitignore @@ -0,0 +1 @@ +/config.yml diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb index 49b2e945c3..07de544868 100644 --- a/activerecord/test/cases/adapter_test.rb +++ b/activerecord/test/cases/adapter_test.rb @@ -43,7 +43,7 @@ class AdapterTest < ActiveRecord::TestCase def test_current_database if @connection.respond_to?(:current_database) - assert_equal ENV['ARUNIT_DB_NAME'] || "activerecord_unittest", @connection.current_database + assert_equal ARTest.connection_config['arunit']['database'], @connection.current_database end end @@ -68,7 +68,12 @@ class AdapterTest < ActiveRecord::TestCase begin assert_nothing_raised do ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['arunit'].except(:database)) - ActiveRecord::Base.connection.execute "SELECT activerecord_unittest.pirates.*, activerecord_unittest2.courses.* FROM activerecord_unittest.pirates, activerecord_unittest2.courses" + + config = ARTest.connection_config + ActiveRecord::Base.connection.execute( + "SELECT #{config['arunit']['database']}.pirates.*, #{config['arunit2']['database']}.courses.* " \ + "FROM #{config['arunit']['database']}.pirates, #{config['arunit2']['database']}.courses" + ) end ensure ActiveRecord::Base.establish_connection 'arunit' diff --git a/activerecord/test/cases/adapters/firebird/migration_test.rb b/activerecord/test/cases/adapters/firebird/migration_test.rb index 710661b9bd..5c94593765 100644 --- a/activerecord/test/cases/adapters/firebird/migration_test.rb +++ b/activerecord/test/cases/adapters/firebird/migration_test.rb @@ -24,7 +24,7 @@ class FirebirdMigrationTest < ActiveRecord::TestCase assert !sequence_exists?('foo_seq') assert sequence_exists?('foo_custom_seq') - assert_nothing_raised { @connection.drop_table(:foo, :sequence => 'foo_custom_seq') } + assert_nothing_raised { @connection.drop_table(:foo) } assert !sequence_exists?('foo_custom_seq') ensure FireRuby::Generator.new('foo_custom_seq', @fireruby_connection).drop rescue nil diff --git a/activerecord/test/cases/adapters/mysql/quoting_test.rb b/activerecord/test/cases/adapters/mysql/quoting_test.rb index 9673e2bb46..3d1330efb8 100644 --- a/activerecord/test/cases/adapters/mysql/quoting_test.rb +++ b/activerecord/test/cases/adapters/mysql/quoting_test.rb @@ -23,4 +23,3 @@ module ActiveRecord end end end - diff --git a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb index 752b864818..3a9744e78f 100644 --- a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb +++ b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb @@ -87,8 +87,8 @@ class MysqlReservedWordTest < ActiveRecord::TestCase assert_nothing_raised { x.save } x.order = 'y' assert_nothing_raised { x.save } - assert_nothing_raised { y = Group.find_by_order('y') } - assert_nothing_raised { y = Group.find(1) } + assert_nothing_raised { Group.find_by_order('y') } + assert_nothing_raised { Group.find(1) } x = Group.find(1) end diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb index 7c49236854..d57794daf8 100644 --- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb +++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb @@ -10,6 +10,45 @@ module ActiveRecord @connection.exec_query('create table ex(id serial primary key, number integer, data character varying(255))') end + def test_primary_key + assert_equal 'id', @connection.primary_key('ex') + end + + def test_non_standard_primary_key + @connection.exec_query('drop table if exists ex') + @connection.exec_query('create table ex(data character varying(255) primary key)') + assert_equal 'data', @connection.primary_key('ex') + end + + def test_primary_key_returns_nil_for_no_pk + @connection.exec_query('drop table if exists ex') + @connection.exec_query('create table ex(id integer)') + assert_nil @connection.primary_key('ex') + end + + def test_primary_key_raises_error_if_table_not_found + assert_raises(ActiveRecord::StatementInvalid) do + @connection.primary_key('unobtainium') + end + end + + def test_insert_sql_with_proprietary_returning_clause + id = @connection.insert_sql("insert into ex (number) values(5150)", nil, "number") + assert_equal "5150", id + end + + def test_insert_sql_with_quoted_schema_and_table_name + id = @connection.insert_sql('insert into "public"."ex" (number) values(5150)') + expect = @connection.query('select max(id) from ex').first.first + assert_equal expect, id + end + + def test_insert_sql_with_no_space_after_table_name + id = @connection.insert_sql("insert into ex(number) values(5150)") + expect = @connection.query('select max(id) from ex').first.first + assert_equal expect, id + end + def test_serial_sequence assert_equal 'public.accounts_id_seq', @connection.serial_sequence('accounts', 'id') @@ -35,6 +74,36 @@ module ActiveRecord @connection.default_sequence_name('zomg') end + def test_pk_and_sequence_for + pk, seq = @connection.pk_and_sequence_for('ex') + assert_equal 'id', pk + assert_equal @connection.default_sequence_name('ex', 'id'), seq + end + + def test_pk_and_sequence_for_with_non_standard_primary_key + @connection.exec_query('drop table if exists ex') + @connection.exec_query('create table ex(code serial primary key)') + pk, seq = @connection.pk_and_sequence_for('ex') + assert_equal 'code', pk + assert_equal @connection.default_sequence_name('ex', 'code'), seq + end + + def test_pk_and_sequence_for_returns_nil_if_no_seq + @connection.exec_query('drop table if exists ex') + @connection.exec_query('create table ex(id integer primary key)') + assert_nil @connection.pk_and_sequence_for('ex') + end + + def test_pk_and_sequence_for_returns_nil_if_no_pk + @connection.exec_query('drop table if exists ex') + @connection.exec_query('create table ex(id integer)') + assert_nil @connection.pk_and_sequence_for('ex') + end + + def test_pk_and_sequence_for_returns_nil_if_table_not_found + assert_nil @connection.pk_and_sequence_for('unobtainium') + end + def test_exec_insert_number insert(@connection, 'number' => 10) diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb index a5c3e69af9..27e7b1a1c3 100644 --- a/activerecord/test/cases/adapters/postgresql/schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb @@ -20,6 +20,7 @@ class SchemaTest < ActiveRecord::TestCase 'email character varying(50)', 'moment timestamp without time zone default now()' ] + PK_TABLE_NAME = 'table_with_pk' class Thing1 < ActiveRecord::Base set_table_name "test_schema.things" @@ -49,6 +50,7 @@ class SchemaTest < ActiveRecord::TestCase @connection.execute "CREATE INDEX #{INDEX_B_NAME} ON #{SCHEMA2_NAME}.#{TABLE_NAME} USING btree (#{INDEX_B_COLUMN_S2});" @connection.execute "CREATE INDEX #{INDEX_C_NAME} ON #{SCHEMA_NAME}.#{TABLE_NAME} USING gin (#{INDEX_C_COLUMN});" @connection.execute "CREATE INDEX #{INDEX_C_NAME} ON #{SCHEMA2_NAME}.#{TABLE_NAME} USING gin (#{INDEX_C_COLUMN});" + @connection.execute "CREATE TABLE #{SCHEMA_NAME}.#{PK_TABLE_NAME} (id serial primary key)" end def teardown @@ -63,12 +65,30 @@ class SchemaTest < ActiveRecord::TestCase end end + def test_table_exists_when_on_schema_search_path + with_schema_search_path(SCHEMA_NAME) do + assert(@connection.table_exists?(TABLE_NAME), "table should exist and be found") + end + end + + def test_table_exists_when_not_on_schema_search_path + with_schema_search_path('PUBLIC') do + assert(!@connection.table_exists?(TABLE_NAME), "table exists but should not be found") + end + end + def test_table_exists_wrong_schema assert(!@connection.table_exists?("foo.things"), "table should not exist") end - def test_table_exists_quoted_table - assert(@connection.table_exists?('"things.table"'), "table should exist") + def test_table_exists_quoted_names + [ %("#{SCHEMA_NAME}"."#{TABLE_NAME}"), %(#{SCHEMA_NAME}."#{TABLE_NAME}"), %(#{SCHEMA_NAME}."#{TABLE_NAME}")].each do |given| + assert(@connection.table_exists?(given), "table should exist when specified as #{given}") + end + with_schema_search_path(SCHEMA_NAME) do + given = %("#{TABLE_NAME}") + assert(@connection.table_exists?(given), "table should exist when specified as #{given}") + end end def test_with_schema_prefixed_table_name @@ -91,7 +111,6 @@ class SchemaTest < ActiveRecord::TestCase end end - def test_proper_encoding_of_table_name assert_equal '"table_name"', @connection.quote_table_name('table_name') assert_equal '"table.name"', @connection.quote_table_name('"table.name"') @@ -164,6 +183,79 @@ class SchemaTest < ActiveRecord::TestCase ActiveRecord::Base.connection.schema_search_path = "public" end + def test_primary_key_with_schema_specified + [ + %("#{SCHEMA_NAME}"."#{PK_TABLE_NAME}"), + %(#{SCHEMA_NAME}."#{PK_TABLE_NAME}"), + %(#{SCHEMA_NAME}.#{PK_TABLE_NAME}) + ].each do |given| + assert_equal 'id', @connection.primary_key(given), "primary key should be found when table referenced as #{given}" + end + end + + def test_primary_key_assuming_schema_search_path + with_schema_search_path(SCHEMA_NAME) do + assert_equal 'id', @connection.primary_key(PK_TABLE_NAME), "primary key should be found" + end + end + + def test_primary_key_raises_error_if_table_not_found_on_schema_search_path + with_schema_search_path(SCHEMA2_NAME) do + assert_raises(ActiveRecord::StatementInvalid) do + @connection.primary_key(PK_TABLE_NAME) + end + end + end + + def test_pk_and_sequence_for_with_schema_specified + [ + %("#{SCHEMA_NAME}"."#{PK_TABLE_NAME}"), + %(#{SCHEMA_NAME}."#{PK_TABLE_NAME}"), + %(#{SCHEMA_NAME}.#{PK_TABLE_NAME}) + ].each do |given| + pk, seq = @connection.pk_and_sequence_for(given) + assert_equal 'id', pk, "primary key should be found when table referenced as #{given}" + assert_equal "#{SCHEMA_NAME}.#{PK_TABLE_NAME}_id_seq", seq, "sequence name should be found when table referenced as #{given}" + end + end + + def test_extract_schema_and_table + { + %(table_name) => [nil,'table_name'], + %("table.name") => [nil,'table.name'], + %(schema.table_name) => %w{schema table_name}, + %("schema".table_name) => %w{schema table_name}, + %(schema."table_name") => %w{schema table_name}, + %("schema"."table_name") => %w{schema table_name}, + %("even spaces".table) => ['even spaces','table'], + %(schema."table.name") => ['schema', 'table.name'] + }.each do |given,expect| + assert_equal expect, @connection.send(:extract_schema_and_table, given) + end + end + + def test_current_schema + { + %('$user',public) => 'public', + SCHEMA_NAME => SCHEMA_NAME, + %(#{SCHEMA2_NAME},#{SCHEMA_NAME},public) => SCHEMA2_NAME, + %(public,#{SCHEMA2_NAME},#{SCHEMA_NAME}) => 'public' + }.each do |given,expect| + with_schema_search_path(given) { assert_equal expect, @connection.current_schema } + end + end + + def test_schema_exists? + { + 'public' => true, + SCHEMA_NAME => true, + SCHEMA2_NAME => true, + 'darkside' => false + }.each do |given,expect| + assert_equal expect, @connection.schema_exists?(given) + end + end + private def columns(table_name) @connection.send(:column_definitions, table_name).map do |name, type, default| diff --git a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb new file mode 100644 index 0000000000..337f43c421 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb @@ -0,0 +1,30 @@ +require 'cases/helper' +require 'models/developer' + +class TimestampTest < ActiveRecord::TestCase + def test_load_infinity_and_beyond + unless current_adapter?(:PostgreSQLAdapter) + return skip("only tested on postgresql") + end + + d = Developer.find_by_sql("select 'infinity'::timestamp as updated_at") + assert d.first.updated_at.infinite?, 'timestamp should be infinite' + + d = Developer.find_by_sql("select '-infinity'::timestamp as updated_at") + time = d.first.updated_at + assert time.infinite?, 'timestamp should be infinite' + assert_operator time, :<, 0 + end + + def test_save_infinity_and_beyond + unless current_adapter?(:PostgreSQLAdapter) + return skip("only tested on postgresql") + end + + d = Developer.create!(:name => 'aaron', :updated_at => 1.0 / 0.0) + assert_equal(1.0 / 0.0, d.updated_at) + + d = Developer.create!(:name => 'aaron', :updated_at => -1.0 / 0.0) + assert_equal(-1.0 / 0.0, d.updated_at) + end +end diff --git a/activerecord/test/cases/adapters/postgresql/view_test.rb b/activerecord/test/cases/adapters/postgresql/view_test.rb new file mode 100644 index 0000000000..303ba9245a --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/view_test.rb @@ -0,0 +1,49 @@ +require "cases/helper" + +class ViewTest < ActiveRecord::TestCase + self.use_transactional_fixtures = false + + SCHEMA_NAME = 'test_schema' + TABLE_NAME = 'things' + VIEW_NAME = 'view_things' + COLUMNS = [ + 'id integer', + 'name character varying(50)', + 'email character varying(50)', + 'moment timestamp without time zone' + ] + + class ThingView < ActiveRecord::Base + set_table_name 'test_schema.view_things' + end + + def setup + @connection = ActiveRecord::Base.connection + @connection.execute "CREATE SCHEMA #{SCHEMA_NAME} CREATE TABLE #{TABLE_NAME} (#{COLUMNS.join(',')})" + @connection.execute "CREATE TABLE #{SCHEMA_NAME}.\"#{TABLE_NAME}.table\" (#{COLUMNS.join(',')})" + @connection.execute "CREATE VIEW #{SCHEMA_NAME}.#{VIEW_NAME} AS SELECT id,name,email,moment FROM #{SCHEMA_NAME}.#{TABLE_NAME}" + end + + def teardown + @connection.execute "DROP SCHEMA #{SCHEMA_NAME} CASCADE" + end + + def test_table_exists + name = ThingView.table_name + assert @connection.table_exists?(name), "'#{name}' table should exist" + end + + def test_column_definitions + assert_nothing_raised do + assert_equal COLUMNS, columns("#{SCHEMA_NAME}.#{VIEW_NAME}") + end + end + + private + def columns(table_name) + @connection.send(:column_definitions, table_name).map do |name, type, default| + "#{name} #{type}" + (default ? " default #{default}" : '') + end + end + +end diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index 5a900e0605..7338513b41 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -296,6 +296,15 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_equal 1, Topic.find(topic.id)[:replies_count] end + def test_belongs_to_counter_when_update_column + topic = Topic.create!(:title => "37s") + topic.replies.create!(:title => "re: 37s", :content => "rails") + assert_equal 1, Topic.find(topic.id)[:replies_count] + + topic.update_column(:content, "rails is wonderfull") + assert_equal 1, Topic.find(topic.id)[:replies_count] + end + def test_assignment_before_child_saved final_cut = Client.new("name" => "Final Cut") firm = Firm.find(1) diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb index 49d8722aff..ff376a68d8 100644 --- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb +++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb @@ -8,10 +8,12 @@ require 'models/company' require 'models/topic' require 'models/reply' require 'models/person' +require 'models/vertex' +require 'models/edge' class CascadedEagerLoadingTest < ActiveRecord::TestCase fixtures :authors, :mixins, :companies, :posts, :topics, :accounts, :comments, - :categorizations, :people, :categories + :categorizations, :people, :categories, :edges, :vertices def test_eager_association_loading_with_cascaded_two_levels authors = Author.find(:all, :include=>{:posts=>:comments}, :order=>"authors.id") @@ -164,12 +166,6 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase authors[2].post_about_thinking.comments.first end end -end - -require 'models/vertex' -require 'models/edge' -class CascadedEagerLoadingTest < ActiveRecord::TestCase - fixtures :edges, :vertices def test_eager_association_loading_with_recursive_cascading_four_levels_has_many_through source = Vertex.find(:first, :include=>{:sinks=>{:sinks=>{:sinks=>:sinks}}}, :order => 'vertices.id') diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index 3e92a77830..325fc58958 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -380,6 +380,18 @@ class EagerAssociationTest < ActiveRecord::TestCase assert_equal subscriptions, subscriber.subscriptions.sort_by(&:id) end + def test_string_id_column_joins + s = Subscriber.create! do |c| + c.id = "PL" + end + + b = Book.create! + + Subscription.create!(:subscriber_id => "PL", :book_id => b.id) + s.reload + s.book_ids = s.book_ids + end + def test_eager_load_has_many_through_with_string_keys books = books(:awdr, :rfr) subscriber = Subscriber.find(subscribers(:second).id, :include => :books) @@ -448,6 +460,12 @@ class EagerAssociationTest < ActiveRecord::TestCase assert_equal post_tags, eager_post_tags end + def test_eager_with_has_many_through_join_model_ignores_default_includes + assert_nothing_raised do + authors(:david).comments_on_posts_with_default_include.to_a + end + end + def test_eager_with_has_many_and_limit posts = Post.find(:all, :order => 'posts.id asc', :include => [ :author, :comments ], :limit => 2) assert_equal 2, posts.size @@ -675,6 +693,46 @@ class EagerAssociationTest < ActiveRecord::TestCase } end + def test_eager_with_default_scope + developer = EagerDeveloperWithDefaultScope.where(:name => 'David').first + projects = Project.order(:id).all + assert_no_queries do + assert_equal(projects, developer.projects) + end + end + + def test_eager_with_default_scope_as_class_method + developer = EagerDeveloperWithClassMethodDefaultScope.where(:name => 'David').first + projects = Project.order(:id).all + assert_no_queries do + assert_equal(projects, developer.projects) + end + end + + def test_eager_with_default_scope_as_lambda + developer = EagerDeveloperWithLambdaDefaultScope.where(:name => 'David').first + projects = Project.order(:id).all + assert_no_queries do + assert_equal(projects, developer.projects) + end + end + + def test_eager_with_default_scope_as_block + developer = EagerDeveloperWithBlockDefaultScope.where(:name => 'David').first + projects = Project.order(:id).all + assert_no_queries do + assert_equal(projects, developer.projects) + end + end + + def test_eager_with_default_scope_as_callable + developer = EagerDeveloperWithCallableDefaultScope.where(:name => 'David').first + projects = Project.order(:id).all + assert_no_queries do + assert_equal(projects, developer.projects) + end + end + def find_all_ordered(className, include=nil) className.find(:all, :order=>"#{className.table_name}.#{className.primary_key}", :include=>include) end diff --git a/activerecord/test/cases/associations/habtm_join_table_test.rb b/activerecord/test/cases/associations/habtm_join_table_test.rb index 745f169ad7..fe2b82f2c1 100644 --- a/activerecord/test/cases/associations/habtm_join_table_test.rb +++ b/activerecord/test/cases/associations/habtm_join_table_test.rb @@ -32,13 +32,4 @@ class HabtmJoinTableTest < ActiveRecord::TestCase 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::HasAndBelongsToManyAssociationWithPrimaryKeyError do - MyReader.has_and_belongs_to_many :my_books - end - end - end end diff --git a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb index f4d14853d3..d8d2a113ff 100644 --- a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb @@ -101,6 +101,16 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal 't1', record[1] end + def test_proper_usage_of_primary_keys_and_join_table + setup_data_for_habtm_case + + assert_equal 'country_id', Country.primary_key + assert_equal 'treaty_id', Treaty.primary_key + + country = Country.first + assert_equal 1, country.treaties.count + end + def test_has_and_belongs_to_many david = Developer.find(1) @@ -235,6 +245,21 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal Developer.find(1).projects.sort_by(&:id).last, proj # prove join table is updated end + def test_new_aliased_to_build + devel = Developer.find(1) + proj = assert_no_queries { devel.projects.new("name" => "Projekt") } + assert !devel.projects.loaded? + + assert_equal devel.projects.last, proj + assert devel.projects.loaded? + + assert !proj.persisted? + devel.save + assert proj.persisted? + assert_equal devel.projects.last, proj + assert_equal Developer.find(1).projects.sort_by(&:id).last, proj # prove join table is updated + end + def test_build_by_new_record devel = Developer.new(:name => "Marcel", :salary => 75000) devel.projects.build(:name => "Make bed") diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 43974fd895..a2764f3e3b 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -2,6 +2,7 @@ require "cases/helper" require 'models/developer' require 'models/project' require 'models/company' +require 'models/contract' require 'models/topic' require 'models/reply' require 'models/category' @@ -225,6 +226,10 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal 2, Firm.find(:first, :order => "id").clients.length end + def test_finding_array_compatibility + assert_equal 2, Firm.order(:id).find{|f| f.id > 0}.clients.length + end + def test_find_with_blank_conditions [[], {}, nil, ""].each do |blank| assert_equal 2, Firm.find(:first, :order => "id").clients.find(:all, :conditions => blank).size @@ -536,6 +541,35 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal 3, companies(:first_firm).clients_of_firm(true).size end + def test_transactions_when_adding_to_persisted + good = Client.new(:name => "Good") + bad = Client.new(:name => "Bad", :raise_on_save => true) + + begin + companies(:first_firm).clients_of_firm.concat(good, bad) + rescue Client::RaisedOnSave + end + + assert !companies(:first_firm).clients_of_firm(true).include?(good) + end + + def test_transactions_when_adding_to_new_record + assert_no_queries do + firm = Firm.new + firm.clients_of_firm.concat(Client.new("name" => "Natural Company")) + end + end + + def test_new_aliased_to_build + company = companies(:first_firm) + new_client = assert_no_queries { company.clients_of_firm.new("name" => "Another Client") } + assert !company.clients_of_firm.loaded? + + assert_equal "Another Client", new_client.name + assert !new_client.persisted? + assert_equal new_client, company.clients_of_firm.last + end + def test_build company = companies(:first_firm) new_client = assert_no_queries { company.clients_of_firm.build("name" => "Another Client") } @@ -767,6 +801,29 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal 0, companies(:first_firm).clients_of_firm(true).size end + def test_transaction_when_deleting_persisted + good = Client.new(:name => "Good") + bad = Client.new(:name => "Bad", :raise_on_destroy => true) + + companies(:first_firm).clients_of_firm = [good, bad] + + begin + companies(:first_firm).clients_of_firm.destroy(good, bad) + rescue Client::RaisedOnDestroy + end + + assert_equal [good, bad], companies(:first_firm).clients_of_firm(true) + end + + def test_transaction_when_deleting_new_record + assert_no_queries do + firm = Firm.new + client = Client.new("name" => "New Client") + firm.clients_of_firm << client + firm.clients_of_firm.destroy(client) + end + end + def test_clearing_an_association_collection firm = companies(:first_firm) client_id = firm.clients_of_firm.first.id @@ -1100,6 +1157,27 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal orig_accounts, firm.accounts end + def test_transactions_when_replacing_on_persisted + good = Client.new(:name => "Good") + bad = Client.new(:name => "Bad", :raise_on_save => true) + + companies(:first_firm).clients_of_firm = [good] + + begin + companies(:first_firm).clients_of_firm = [bad] + rescue Client::RaisedOnSave + end + + assert_equal [good], companies(:first_firm).clients_of_firm(true) + end + + def test_transactions_when_replacing_on_new_record + assert_no_queries do + firm = Firm.new + firm.clients_of_firm = [Client.new("name" => "New Client")] + end + end + def test_get_ids assert_equal [companies(:first_client).id, companies(:second_client).id], companies(:first_firm).client_ids end @@ -1475,4 +1553,19 @@ class HasManyAssociationsTest < ActiveRecord::TestCase tagging = Tagging.create! :taggable => post assert_equal [tagging], post.taggings end + + def test_dont_call_save_callbacks_twice_on_has_many + firm = companies(:first_firm) + contract = firm.contracts.create! + + assert_equal 1, contract.hi_count + assert_equal 1, contract.bye_count + end + + def test_association_attributes_are_available_to_after_initialize + car = Car.create(:name => 'honda') + bulb = car.bulbs.build + + assert_equal car.id, bulb.attributes_after_initialize['car_id'] + end end 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 89117593fd..877148bd5e 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -766,4 +766,46 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert_equal [category.name], post.named_category_ids # checks when target loaded assert_equal [category.name], post.reload.named_category_ids # checks when target no loaded end + + def test_create_should_not_raise_exception_when_join_record_has_errors + repair_validations(Categorization) do + Categorization.validate { |r| r.errors[:base] << 'Invalid Categorization' } + Category.create(:name => 'Fishing', :authors => [Author.first]) + end + end + + def test_save_should_not_raise_exception_when_join_record_has_errors + repair_validations(Categorization) do + Categorization.validate { |r| r.errors[:base] << 'Invalid Categorization' } + c = Category.create(:name => 'Fishing', :authors => [Author.first]) + c.save + end + end + + def test_create_bang_should_raise_exception_when_join_record_has_errors + repair_validations(Categorization) do + Categorization.validate { |r| r.errors[:base] << 'Invalid Categorization' } + assert_raises(ActiveRecord::RecordInvalid) do + Category.create!(:name => 'Fishing', :authors => [Author.first]) + end + end + end + + def test_save_bang_should_raise_exception_when_join_record_has_errors + repair_validations(Categorization) do + Categorization.validate { |r| r.errors[:base] << 'Invalid Categorization' } + c = Category.new(:name => 'Fishing', :authors => [Author.first]) + assert_raises(ActiveRecord::RecordInvalid) do + c.save! + end + end + end + + def test_create_bang_returns_falsy_when_join_record_has_errors + repair_validations(Categorization) do + Categorization.validate { |r| r.errors[:base] << 'Invalid Categorization' } + c = Category.new(:name => 'Fishing', :authors => [Author.first]) + assert !c.save + end + end end diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index f3bf5baa95..26931e3e85 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -345,6 +345,17 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert orig_ship.destroyed? end + def test_creation_failure_due_to_new_record_should_raise_error + pirate = pirates(:redbeard) + new_ship = Ship.new + + assert_raise(ActiveRecord::RecordNotSaved) do + pirate.ship = new_ship + end + assert_nil pirate.ship + assert_nil new_ship.pirate_id + end + def test_replacement_failure_due_to_existing_record_should_raise_error pirate = pirates(:blackbeard) pirate.ship.name = nil @@ -370,15 +381,6 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert_nil new_ship.pirate_id end - def test_deprecated_association_loaded - firm = companies(:first_firm) - firm.association(:account).stubs(:loaded?).returns(stub) - - assert_deprecated do - assert_equal firm.association(:account).loaded?, firm.account_loaded? - end - end - def test_association_keys_bypass_attribute_protection car = Car.create(:name => 'honda') @@ -447,4 +449,11 @@ class HasOneAssociationsTest < ActiveRecord::TestCase bulb = car.create_bulb!{ |b| b.color = 'Red' } assert_equal 'RED!', bulb.color end + + def test_association_attributes_are_available_to_after_initialize + car = Car.create(:name => 'honda') + bulb = car.create_bulb + + assert_equal car.id, bulb.attributes_after_initialize['car_id'] + end end diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb index b59ce4efeb..4ce8b85098 100644 --- a/activerecord/test/cases/associations/join_model_test.rb +++ b/activerecord/test/cases/associations/join_model_test.rb @@ -139,7 +139,21 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase def test_set_polymorphic_has_one tagging = tags(:misc).taggings.create posts(:thinking).tagging = tagging - assert_equal "Post", tagging.taggable_type + + assert_equal "Post", tagging.taggable_type + assert_equal posts(:thinking).id, tagging.taggable_id + assert_equal posts(:thinking), tagging.taggable + end + + def test_set_polymorphic_has_one_on_new_record + tagging = tags(:misc).taggings.create + post = Post.new :title => "foo", :body => "bar" + post.tagging = tagging + post.save! + + assert_equal "Post", tagging.taggable_type + assert_equal post.id, tagging.taggable_id + assert_equal post, tagging.taggable end def test_create_polymorphic_has_many_with_scope @@ -708,8 +722,8 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_has_many_with_pluralize_table_names_false - engine = Engine.create!(:car_id => 1) - aircraft = Aircraft.create!(:name => "Airbus 380", :id => 1) + aircraft = Aircraft.create!(:name => "Airbus 380") + engine = Engine.create!(:car_id => aircraft.id) assert_equal aircraft.engines, [engine] end diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb index 38d439d68a..49d82ba2df 100644 --- a/activerecord/test/cases/associations_test.rb +++ b/activerecord/test/cases/associations_test.rb @@ -203,18 +203,6 @@ class AssociationProxyTest < ActiveRecord::TestCase assert_equal david.projects, david.projects.reload.reload end end - - # Tests that proxy_owner, proxy_target and proxy_reflection are implement as deprecated methods - def test_proxy_deprecations - david = developers(:david) - david.projects.load_target - - [:owner, :target, :reflection].each do |name| - assert_deprecated do - assert_equal david.association(:projects).send(name), david.projects.send("proxy_#{name}") - end - end - end end class OverridingAssociationsTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index 5074ae50ab..b0896fb236 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -109,6 +109,14 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_respond_to topic, :title end + # IRB inspects the return value of "MyModel.allocate" + # by inspecting it. + def test_allocated_object_can_be_inspected + topic = Topic.allocate + assert_nothing_raised { topic.inspect } + assert topic.inspect, "#<Topic not initialized>" + end + def test_array_content topic = Topic.new topic.content = %w( one two three ) @@ -126,7 +134,12 @@ class AttributeMethodsTest < ActiveRecord::TestCase if current_adapter?(:MysqlAdapter) def test_read_attributes_before_type_cast_on_boolean bool = Boolean.create({ "value" => false }) - assert_equal 0, bool.reload.attributes_before_type_cast["value"] + if RUBY_PLATFORM =~ /java/ + # JRuby will return the value before typecast as string + assert_equal "0", bool.reload.attributes_before_type_cast["value"] + else + assert_equal 0, bool.reload.attributes_before_type_cast["value"] + end end end diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index 8f55b7ebe6..4ad2cdfc7e 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -837,7 +837,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase @pirate.parrots.each { |parrot| parrot.mark_for_destruction } assert @pirate.save - assert_no_queries do + assert_queries(0) do assert @pirate.save end end diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 87ce537e0e..5fef3faab9 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -478,6 +478,19 @@ class BasicsTest < ActiveRecord::TestCase def test_hashing assert_equal [ Topic.find(1) ], [ Topic.find(2).topic ] & [ Topic.find(1) ] end + + def test_comparison + topic_1 = Topic.create! + topic_2 = Topic.create! + + assert_equal [topic_2, topic_1].sort, [topic_1, topic_2] + end + + def test_comparison_with_different_objects + topic = Topic.create + category = Category.create(:name => "comparison") + assert_nil topic <=> category + end def test_readonly_attributes assert_equal Set.new([ 'title' , 'comments_count' ]), ReadonlyTitlePost.readonly_attributes @@ -502,13 +515,6 @@ class BasicsTest < ActiveRecord::TestCase assert_equal 'value2', weird.send('a$b') end - def test_attributes_guard_protected_attributes_is_deprecated - attributes = { "title" => "An amazing title" } - post = ProtectedTitlePost.new - assert_deprecated { post.send(:attributes=, attributes, false) } - assert_equal "An amazing title", post.title - end - def test_multiparameter_attributes_on_date attributes = { "last_read(1i)" => "2004", "last_read(2i)" => "6", "last_read(3i)" => "24" } topic = Topic.find(1) @@ -1773,6 +1779,13 @@ class BasicsTest < ActiveRecord::TestCase end end + def test_compute_type_argument_error + ActiveSupport::Dependencies.stubs(:constantize).raises(ArgumentError) + assert_raises ArgumentError do + ActiveRecord::Base.send :compute_type, 'InvalidModel' + end + end + def test_clear_cache! # preheat cache c1 = Post.columns diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index 56f6d795b6..224b3f3d1f 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -397,6 +397,10 @@ class CalculationsTest < ActiveRecord::TestCase Account.sum(:credit_limit, :from => 'accounts', :conditions => "credit_limit > 50") end + def test_sum_array_compatibility + assert_equal Account.sum(:credit_limit), Account.sum(&:credit_limit) + end + def test_average_with_from_option assert_equal Account.average(:credit_limit), Account.average(:credit_limit, :from => 'accounts') assert_equal Account.average(:credit_limit, :conditions => "credit_limit > 50"), diff --git a/activerecord/test/cases/connection_management_test.rb b/activerecord/test/cases/connection_management_test.rb index 85871aebdf..a1d1177289 100644 --- a/activerecord/test/cases/connection_management_test.rb +++ b/activerecord/test/cases/connection_management_test.rb @@ -77,6 +77,13 @@ module ActiveRecord @management.call(@env) assert ActiveRecord::Base.connection_handler.active_connections? end + + test "proxy is polite to it's body and responds to it" do + body = Class.new(String) { def to_path; "/path"; end }.new + proxy = ConnectionManagement::Proxy.new(body) + assert proxy.respond_to?(:to_path) + assert_equal proxy.to_path, "/path" + end end end end diff --git a/activerecord/test/cases/fixtures/file_test.rb b/activerecord/test/cases/fixtures/file_test.rb index 8dbf92ae9a..e623fbe4d1 100644 --- a/activerecord/test/cases/fixtures/file_test.rb +++ b/activerecord/test/cases/fixtures/file_test.rb @@ -1,4 +1,4 @@ -require "cases/helper" +require 'cases/helper' require 'tempfile' module ActiveRecord diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index 306b437fb3..854eb86c09 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -589,8 +589,8 @@ class FoxyFixturesTest < ActiveRecord::TestCase end def test_preserves_existing_fixture_data - assert_equal(2.weeks.ago.to_date, pirates(:redbeard).created_on.to_date) - assert_equal(2.weeks.ago.to_date, pirates(:redbeard).updated_on.to_date) + assert_equal(2.weeks.ago.utc.to_date, pirates(:redbeard).created_on.utc.to_date) + assert_equal(2.weeks.ago.utc.to_date, pirates(:redbeard).updated_on.utc.to_date) end def test_generates_unique_ids diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index fbb4ee6f7b..6735bc521b 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -1,8 +1,5 @@ require File.expand_path('../../../../load_paths', __FILE__) -lib = File.expand_path("#{File.dirname(__FILE__)}/../../lib") -$:.unshift(lib) unless $:.include?('lib') || $:.include?(lib) - require 'config' require 'test/unit' @@ -11,24 +8,24 @@ require 'mocha' require 'active_record' require 'active_support/dependencies' -begin - require 'connection' -rescue LoadError - # If we cannot load connection we assume that driver was not loaded for this test case, so we load sqlite3 as default one. - # This allows for running separate test cases by simply running test file. - connection_type = defined?(JRUBY_VERSION) ? 'jdbc' : 'native' - require "test/connections/#{connection_type}_sqlite3/connection" -end + +require 'support/config' +require 'support/connection' + +# TODO: Move all these random hacks into the ARTest namespace and into the support/ dir # Show backtraces for deprecated behavior for quicker cleanup. ActiveSupport::Deprecation.debug = true +# Enable Identity Map only when ENV['IM'] is set to "true" +ActiveRecord::IdentityMap.enabled = (ENV['IM'] == "true") + +# Connect to the database +ARTest.connect + # Quote "type" if it's a reserved word for the current connection. QUOTED_TYPE = ActiveRecord::Base.connection.quote_column_name('type') -# Enable Identity Map for testing -ActiveRecord::IdentityMap.enabled = (ENV['IM'] == "false" ? false : true) - def current_adapter?(*types) types.any? do |type| ActiveRecord::ConnectionAdapters.const_defined?(type) && @@ -61,15 +58,15 @@ end module ActiveRecord class SQLCounter - IGNORED_SQL = [/^PRAGMA (?!(table_info))/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/, /^SAVEPOINT/, /^ROLLBACK TO SAVEPOINT/, /^RELEASE SAVEPOINT/, /^SHOW max_identifier_length/] + cattr_accessor :ignored_sql + self.ignored_sql = [/^PRAGMA (?!(table_info))/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/, /^SAVEPOINT/, /^ROLLBACK TO SAVEPOINT/, /^RELEASE SAVEPOINT/, /^SHOW max_identifier_length/, /^BEGIN/, /^COMMIT/] # FIXME: this needs to be refactored so specific database can add their own # ignored SQL. This ignored SQL is for Oracle. - IGNORED_SQL.concat [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im] + ignored_sql.concat [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im] - def initialize - $queries_executed = [] - end + cattr_accessor :log + self.log = [] def call(name, start, finish, message_id, values) sql = values[:sql] @@ -77,10 +74,11 @@ module ActiveRecord # FIXME: this seems bad. we should probably have a better way to indicate # the query was cached unless 'CACHE' == values[:name] - $queries_executed << sql unless IGNORED_SQL.any? { |r| sql =~ r } + self.class.log << sql unless self.class.ignored_sql.any? { |r| sql =~ r } end end end + ActiveSupport::Notifications.subscribe('sql.active_record', SQLCounter.new) end diff --git a/activerecord/test/cases/i18n_test.rb b/activerecord/test/cases/i18n_test.rb index 469f513e68..a428f1d87b 100644 --- a/activerecord/test/cases/i18n_test.rb +++ b/activerecord/test/cases/i18n_test.rb @@ -43,4 +43,3 @@ class ActiveRecordI18nTests < ActiveRecord::TestCase assert_equal 'topic model', Reply.model_name.human end end - diff --git a/activerecord/test/cases/identity_map_test.rb b/activerecord/test/cases/identity_map_test.rb index a0e16400d2..cd9c358799 100644 --- a/activerecord/test/cases/identity_map_test.rb +++ b/activerecord/test/cases/identity_map_test.rb @@ -140,7 +140,7 @@ class IdentityMapTest < ActiveRecord::TestCase assert_not_same(p1, p2) end end - + def test_inherited_with_type_attribute_without_identity_map ActiveRecord::IdentityMap.without do c = comments(:sub_special_comment) diff --git a/activerecord/test/cases/json_serialization_test.rb b/activerecord/test/cases/json_serialization_test.rb index 8664d63e8f..d9e350abc0 100644 --- a/activerecord/test/cases/json_serialization_test.rb +++ b/activerecord/test/cases/json_serialization_test.rb @@ -161,6 +161,15 @@ class DatabaseConnectedJsonEncodingTest < ActiveRecord::TestCase assert_match %r{"tag":\{"name":"General"\}}, json end + def test_includes_doesnt_merge_opts_from_base + json = @david.to_json( + :only => :id, + :include => :posts + ) + + assert_match %{"title":"Welcome to the weblog"}, json + end + def test_should_not_call_methods_on_associations_that_dont_respond def @david.favorite_quote; "Constraints are liberating"; end json = @david.to_json(:include => :posts, :methods => :favorite_quote) diff --git a/activerecord/test/cases/mass_assignment_security_test.rb b/activerecord/test/cases/mass_assignment_security_test.rb index 33737e12a8..ef35f3341e 100644 --- a/activerecord/test/cases/mass_assignment_security_test.rb +++ b/activerecord/test/cases/mass_assignment_security_test.rb @@ -384,81 +384,81 @@ class MassAssignmentSecurityBelongsToRelationsTest < ActiveRecord::TestCase # build - def test_has_one_build_with_attr_protected_attributes + def test_belongs_to_build_with_attr_protected_attributes best_friend = @person.build_best_friend_of(attributes_hash) assert_default_attributes(best_friend) end - def test_has_one_build_with_attr_accessible_attributes + def test_belongs_to_build_with_attr_accessible_attributes best_friend = @person.build_best_friend_of(attributes_hash) assert_default_attributes(best_friend) end - def test_has_one_build_with_admin_role_with_attr_protected_attributes + def test_belongs_to_build_with_admin_role_with_attr_protected_attributes best_friend = @person.build_best_friend_of(attributes_hash, :as => :admin) assert_admin_attributes(best_friend) end - def test_has_one_build_with_admin_role_with_attr_accessible_attributes + def test_belongs_to_build_with_admin_role_with_attr_accessible_attributes best_friend = @person.build_best_friend_of(attributes_hash, :as => :admin) assert_admin_attributes(best_friend) end - def test_has_one_build_without_protection + def test_belongs_to_build_without_protection best_friend = @person.build_best_friend_of(attributes_hash, :without_protection => true) assert_all_attributes(best_friend) end # create - def test_has_one_create_with_attr_protected_attributes + def test_belongs_to_create_with_attr_protected_attributes best_friend = @person.create_best_friend_of(attributes_hash) assert_default_attributes(best_friend, true) end - def test_has_one_create_with_attr_accessible_attributes + def test_belongs_to_create_with_attr_accessible_attributes best_friend = @person.create_best_friend_of(attributes_hash) assert_default_attributes(best_friend, true) end - def test_has_one_create_with_admin_role_with_attr_protected_attributes + def test_belongs_to_create_with_admin_role_with_attr_protected_attributes best_friend = @person.create_best_friend_of(attributes_hash, :as => :admin) assert_admin_attributes(best_friend, true) end - def test_has_one_create_with_admin_role_with_attr_accessible_attributes + def test_belongs_to_create_with_admin_role_with_attr_accessible_attributes best_friend = @person.create_best_friend_of(attributes_hash, :as => :admin) assert_admin_attributes(best_friend, true) end - def test_has_one_create_without_protection + def test_belongs_to_create_without_protection best_friend = @person.create_best_friend_of(attributes_hash, :without_protection => true) assert_all_attributes(best_friend) end # create! - def test_has_one_create_with_bang_with_attr_protected_attributes + def test_belongs_to_create_with_bang_with_attr_protected_attributes best_friend = @person.create_best_friend!(attributes_hash) assert_default_attributes(best_friend, true) end - def test_has_one_create_with_bang_with_attr_accessible_attributes + def test_belongs_to_create_with_bang_with_attr_accessible_attributes best_friend = @person.create_best_friend!(attributes_hash) assert_default_attributes(best_friend, true) end - def test_has_one_create_with_bang_with_admin_role_with_attr_protected_attributes + def test_belongs_to_create_with_bang_with_admin_role_with_attr_protected_attributes best_friend = @person.create_best_friend!(attributes_hash, :as => :admin) assert_admin_attributes(best_friend, true) end - def test_has_one_create_with_bang_with_admin_role_with_attr_accessible_attributes + def test_belongs_to_create_with_bang_with_admin_role_with_attr_accessible_attributes best_friend = @person.create_best_friend!(attributes_hash, :as => :admin) assert_admin_attributes(best_friend, true) end - def test_has_one_create_with_bang_without_protection + def test_belongs_to_create_with_bang_without_protection best_friend = @person.create_best_friend!(attributes_hash, :without_protection => true) assert_all_attributes(best_friend) end @@ -472,83 +472,328 @@ class MassAssignmentSecurityHasManyRelationsTest < ActiveRecord::TestCase # build - def test_has_one_build_with_attr_protected_attributes + def test_has_many_build_with_attr_protected_attributes best_friend = @person.best_friends.build(attributes_hash) assert_default_attributes(best_friend) end - def test_has_one_build_with_attr_accessible_attributes + def test_has_many_build_with_attr_accessible_attributes best_friend = @person.best_friends.build(attributes_hash) assert_default_attributes(best_friend) end - def test_has_one_build_with_admin_role_with_attr_protected_attributes + def test_has_many_build_with_admin_role_with_attr_protected_attributes best_friend = @person.best_friends.build(attributes_hash, :as => :admin) assert_admin_attributes(best_friend) end - def test_has_one_build_with_admin_role_with_attr_accessible_attributes + def test_has_many_build_with_admin_role_with_attr_accessible_attributes best_friend = @person.best_friends.build(attributes_hash, :as => :admin) assert_admin_attributes(best_friend) end - def test_has_one_build_without_protection + def test_has_many_build_without_protection best_friend = @person.best_friends.build(attributes_hash, :without_protection => true) assert_all_attributes(best_friend) end # create - def test_has_one_create_with_attr_protected_attributes + def test_has_many_create_with_attr_protected_attributes best_friend = @person.best_friends.create(attributes_hash) assert_default_attributes(best_friend, true) end - def test_has_one_create_with_attr_accessible_attributes + def test_has_many_create_with_attr_accessible_attributes best_friend = @person.best_friends.create(attributes_hash) assert_default_attributes(best_friend, true) end - def test_has_one_create_with_admin_role_with_attr_protected_attributes + def test_has_many_create_with_admin_role_with_attr_protected_attributes best_friend = @person.best_friends.create(attributes_hash, :as => :admin) assert_admin_attributes(best_friend, true) end - def test_has_one_create_with_admin_role_with_attr_accessible_attributes + def test_has_many_create_with_admin_role_with_attr_accessible_attributes best_friend = @person.best_friends.create(attributes_hash, :as => :admin) assert_admin_attributes(best_friend, true) end - def test_has_one_create_without_protection + def test_has_many_create_without_protection best_friend = @person.best_friends.create(attributes_hash, :without_protection => true) assert_all_attributes(best_friend) end # create! - def test_has_one_create_with_bang_with_attr_protected_attributes + def test_has_many_create_with_bang_with_attr_protected_attributes best_friend = @person.best_friends.create!(attributes_hash) assert_default_attributes(best_friend, true) end - def test_has_one_create_with_bang_with_attr_accessible_attributes + def test_has_many_create_with_bang_with_attr_accessible_attributes best_friend = @person.best_friends.create!(attributes_hash) assert_default_attributes(best_friend, true) end - def test_has_one_create_with_bang_with_admin_role_with_attr_protected_attributes + def test_has_many_create_with_bang_with_admin_role_with_attr_protected_attributes best_friend = @person.best_friends.create!(attributes_hash, :as => :admin) assert_admin_attributes(best_friend, true) end - def test_has_one_create_with_bang_with_admin_role_with_attr_accessible_attributes + def test_has_many_create_with_bang_with_admin_role_with_attr_accessible_attributes best_friend = @person.best_friends.create!(attributes_hash, :as => :admin) assert_admin_attributes(best_friend, true) end - def test_has_one_create_with_bang_without_protection + def test_has_many_create_with_bang_without_protection best_friend = @person.best_friends.create!(attributes_hash, :without_protection => true) assert_all_attributes(best_friend) end end + + +class MassAssignmentSecurityNestedAttributesTest < ActiveRecord::TestCase + include MassAssignmentTestHelpers + + def nested_attributes_hash(association, collection = false, except = [:id]) + if collection + { :first_name => 'David' }.merge(:"#{association}_attributes" => [attributes_hash.except(*except)]) + else + { :first_name => 'David' }.merge(:"#{association}_attributes" => attributes_hash.except(*except)) + end + end + + # build + + def test_has_one_new_with_attr_protected_attributes + person = LoosePerson.new(nested_attributes_hash(:best_friend)) + assert_default_attributes(person.best_friend) + end + + def test_has_one_new_with_attr_accessible_attributes + person = TightPerson.new(nested_attributes_hash(:best_friend)) + assert_default_attributes(person.best_friend) + end + + def test_has_one_new_with_admin_role_with_attr_protected_attributes + person = LoosePerson.new(nested_attributes_hash(:best_friend), :as => :admin) + assert_admin_attributes(person.best_friend) + end + + def test_has_one_new_with_admin_role_with_attr_accessible_attributes + person = TightPerson.new(nested_attributes_hash(:best_friend), :as => :admin) + assert_admin_attributes(person.best_friend) + end + + def test_has_one_new_without_protection + person = LoosePerson.new(nested_attributes_hash(:best_friend, false, nil), :without_protection => true) + assert_all_attributes(person.best_friend) + end + + def test_belongs_to_new_with_attr_protected_attributes + person = LoosePerson.new(nested_attributes_hash(:best_friend_of)) + assert_default_attributes(person.best_friend_of) + end + + def test_belongs_to_new_with_attr_accessible_attributes + person = TightPerson.new(nested_attributes_hash(:best_friend_of)) + assert_default_attributes(person.best_friend_of) + end + + def test_belongs_to_new_with_admin_role_with_attr_protected_attributes + person = LoosePerson.new(nested_attributes_hash(:best_friend_of), :as => :admin) + assert_admin_attributes(person.best_friend_of) + end + + def test_belongs_to_new_with_admin_role_with_attr_accessible_attributes + person = TightPerson.new(nested_attributes_hash(:best_friend_of), :as => :admin) + assert_admin_attributes(person.best_friend_of) + end + + def test_belongs_to_new_without_protection + person = LoosePerson.new(nested_attributes_hash(:best_friend_of, false, nil), :without_protection => true) + assert_all_attributes(person.best_friend_of) + end + + def test_has_many_new_with_attr_protected_attributes + person = LoosePerson.new(nested_attributes_hash(:best_friends, true)) + assert_default_attributes(person.best_friends.first) + end + + def test_has_many_new_with_attr_accessible_attributes + person = TightPerson.new(nested_attributes_hash(:best_friends, true)) + assert_default_attributes(person.best_friends.first) + end + + def test_has_many_new_with_admin_role_with_attr_protected_attributes + person = LoosePerson.new(nested_attributes_hash(:best_friends, true), :as => :admin) + assert_admin_attributes(person.best_friends.first) + end + + def test_has_many_new_with_admin_role_with_attr_accessible_attributes + person = TightPerson.new(nested_attributes_hash(:best_friends, true), :as => :admin) + assert_admin_attributes(person.best_friends.first) + end + + def test_has_many_new_without_protection + person = LoosePerson.new(nested_attributes_hash(:best_friends, true, nil), :without_protection => true) + assert_all_attributes(person.best_friends.first) + end + + # create + + def test_has_one_create_with_attr_protected_attributes + person = LoosePerson.create(nested_attributes_hash(:best_friend)) + assert_default_attributes(person.best_friend, true) + end + + def test_has_one_create_with_attr_accessible_attributes + person = TightPerson.create(nested_attributes_hash(:best_friend)) + assert_default_attributes(person.best_friend, true) + end + + def test_has_one_create_with_admin_role_with_attr_protected_attributes + person = LoosePerson.create(nested_attributes_hash(:best_friend), :as => :admin) + assert_admin_attributes(person.best_friend, true) + end + + def test_has_one_create_with_admin_role_with_attr_accessible_attributes + person = TightPerson.create(nested_attributes_hash(:best_friend), :as => :admin) + assert_admin_attributes(person.best_friend, true) + end + + def test_has_one_create_without_protection + person = LoosePerson.create(nested_attributes_hash(:best_friend, false, nil), :without_protection => true) + assert_all_attributes(person.best_friend) + end + + def test_belongs_to_create_with_attr_protected_attributes + person = LoosePerson.create(nested_attributes_hash(:best_friend_of)) + assert_default_attributes(person.best_friend_of, true) + end + + def test_belongs_to_create_with_attr_accessible_attributes + person = TightPerson.create(nested_attributes_hash(:best_friend_of)) + assert_default_attributes(person.best_friend_of, true) + end + + def test_belongs_to_create_with_admin_role_with_attr_protected_attributes + person = LoosePerson.create(nested_attributes_hash(:best_friend_of), :as => :admin) + assert_admin_attributes(person.best_friend_of, true) + end + + def test_belongs_to_create_with_admin_role_with_attr_accessible_attributes + person = TightPerson.create(nested_attributes_hash(:best_friend_of), :as => :admin) + assert_admin_attributes(person.best_friend_of, true) + end + + def test_belongs_to_create_without_protection + person = LoosePerson.create(nested_attributes_hash(:best_friend_of, false, nil), :without_protection => true) + assert_all_attributes(person.best_friend_of) + end + + def test_has_many_create_with_attr_protected_attributes + person = LoosePerson.create(nested_attributes_hash(:best_friends, true)) + assert_default_attributes(person.best_friends.first, true) + end + + def test_has_many_create_with_attr_accessible_attributes + person = TightPerson.create(nested_attributes_hash(:best_friends, true)) + assert_default_attributes(person.best_friends.first, true) + end + + def test_has_many_create_with_admin_role_with_attr_protected_attributes + person = LoosePerson.create(nested_attributes_hash(:best_friends, true), :as => :admin) + assert_admin_attributes(person.best_friends.first, true) + end + + def test_has_many_create_with_admin_role_with_attr_accessible_attributes + person = TightPerson.create(nested_attributes_hash(:best_friends, true), :as => :admin) + assert_admin_attributes(person.best_friends.first, true) + end + + def test_has_many_create_without_protection + person = LoosePerson.create(nested_attributes_hash(:best_friends, true, nil), :without_protection => true) + assert_all_attributes(person.best_friends.first) + end + + # create! + + def test_has_one_create_with_bang_with_attr_protected_attributes + person = LoosePerson.create!(nested_attributes_hash(:best_friend)) + assert_default_attributes(person.best_friend, true) + end + + def test_has_one_create_with_bang_with_attr_accessible_attributes + person = TightPerson.create!(nested_attributes_hash(:best_friend)) + assert_default_attributes(person.best_friend, true) + end + + def test_has_one_create_with_bang_with_admin_role_with_attr_protected_attributes + person = LoosePerson.create!(nested_attributes_hash(:best_friend), :as => :admin) + assert_admin_attributes(person.best_friend, true) + end + + def test_has_one_create_with_bang_with_admin_role_with_attr_accessible_attributes + person = TightPerson.create!(nested_attributes_hash(:best_friend), :as => :admin) + assert_admin_attributes(person.best_friend, true) + end + + def test_has_one_create_with_bang_without_protection + person = LoosePerson.create!(nested_attributes_hash(:best_friend, false, nil), :without_protection => true) + assert_all_attributes(person.best_friend) + end + + def test_belongs_to_create_with_bang_with_attr_protected_attributes + person = LoosePerson.create!(nested_attributes_hash(:best_friend_of)) + assert_default_attributes(person.best_friend_of, true) + end + + def test_belongs_to_create_with_bang_with_attr_accessible_attributes + person = TightPerson.create!(nested_attributes_hash(:best_friend_of)) + assert_default_attributes(person.best_friend_of, true) + end + + def test_belongs_to_create_with_bang_with_admin_role_with_attr_protected_attributes + person = LoosePerson.create!(nested_attributes_hash(:best_friend_of), :as => :admin) + assert_admin_attributes(person.best_friend_of, true) + end + + def test_belongs_to_create_with_bang_with_admin_role_with_attr_accessible_attributes + person = TightPerson.create!(nested_attributes_hash(:best_friend_of), :as => :admin) + assert_admin_attributes(person.best_friend_of, true) + end + + def test_belongs_to_create_with_bang_without_protection + person = LoosePerson.create!(nested_attributes_hash(:best_friend_of, false, nil), :without_protection => true) + assert_all_attributes(person.best_friend_of) + end + + def test_has_many_create_with_bang_with_attr_protected_attributes + person = LoosePerson.create!(nested_attributes_hash(:best_friends, true)) + assert_default_attributes(person.best_friends.first, true) + end + + def test_has_many_create_with_bang_with_attr_accessible_attributes + person = TightPerson.create!(nested_attributes_hash(:best_friends, true)) + assert_default_attributes(person.best_friends.first, true) + end + + def test_has_many_create_with_bang_with_admin_role_with_attr_protected_attributes + person = LoosePerson.create!(nested_attributes_hash(:best_friends, true), :as => :admin) + assert_admin_attributes(person.best_friends.first, true) + end + + def test_has_many_create_with_bang_with_admin_role_with_attr_accessible_attributes + person = TightPerson.create!(nested_attributes_hash(:best_friends, true), :as => :admin) + assert_admin_attributes(person.best_friends.first, true) + end + + def test_has_many_create_with_bang_without_protection + person = LoosePerson.create!(nested_attributes_hash(:best_friends, true, nil), :without_protection => true) + assert_all_attributes(person.best_friends.first) + end + +end diff --git a/activerecord/test/cases/migration/command_recorder_test.rb b/activerecord/test/cases/migration/command_recorder_test.rb index ae531ebb4c..36007255fa 100644 --- a/activerecord/test/cases/migration/command_recorder_test.rb +++ b/activerecord/test/cases/migration/command_recorder_test.rb @@ -29,7 +29,12 @@ module ActiveRecord assert_equal [[:create_table, [:horses]]], recorder.commands end - def test_unknown_commands_raise_exception + def test_unknown_commands_delegate + recorder = CommandRecorder.new(stub(:foo => 'bar')) + assert_equal 'bar', recorder.foo + end + + def test_unknown_commands_raise_exception_if_they_cannot_delegate @recorder.record :execute, ['some sql'] assert_raises(ActiveRecord::IrreversibleMigration) do @recorder.inverse @@ -86,6 +91,18 @@ module ActiveRecord assert_equal [:remove_index, [:table, {:column => [:one, :two]}]], remove end + def test_invert_add_index_with_name + @recorder.record :add_index, [:table, [:one, :two], {:name => "new_index"}] + remove = @recorder.inverse.first + assert_equal [:remove_index, [:table, {:name => "new_index"}]], remove + end + + def test_invert_add_index_with_no_options + @recorder.record :add_index, [:table, [:one, :two]] + remove = @recorder.inverse.first + assert_equal [:remove_index, [:table, {:column => [:one, :two]}]], remove + end + def test_invert_rename_index @recorder.record :rename_index, [:old, :new] rename = @recorder.inverse.first diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index bf7565a0d0..93a1249e43 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -1071,6 +1071,18 @@ if ActiveRecord::Base.connection.supports_migrations? Person.connection.drop_table :testings rescue nil end + def test_column_exists_on_table_with_no_options_parameter_supplied + Person.connection.create_table :testings do |t| + t.string :foo + end + Person.connection.change_table :testings do |t| + assert t.column_exists?(:foo) + assert !(t.column_exists?(:bar)) + end + ensure + Person.connection.drop_table :testings rescue nil + end + def test_add_table assert !Reminder.table_exists? diff --git a/activerecord/test/cases/named_scope_test.rb b/activerecord/test/cases/named_scope_test.rb index 34188e4915..2afe3c8f32 100644 --- a/activerecord/test/cases/named_scope_test.rb +++ b/activerecord/test/cases/named_scope_test.rb @@ -456,6 +456,14 @@ class NamedScopeTest < ActiveRecord::TestCase end end + def test_scopes_to_get_newest + post = posts(:welcome) + old_last_comment = post.comments.newest + new_comment = post.comments.create(:body => "My new comment") + assert_equal new_comment, post.comments.newest + assert_not_equal old_last_comment, post.comments.newest + end + def test_scopes_are_reset_on_association_reload post = posts(:welcome) diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb index a61180cfaf..ad17f6f83a 100644 --- a/activerecord/test/cases/query_cache_test.rb +++ b/activerecord/test/cases/query_cache_test.rb @@ -203,3 +203,14 @@ class QueryCacheExpiryTest < ActiveRecord::TestCase end end end + +class QueryCacheBodyProxyTest < ActiveRecord::TestCase + + test "is polite to it's body and responds to it" do + body = Class.new(String) { def to_path; "/path"; end }.new + proxy = ActiveRecord::QueryCache::BodyProxy.new(nil, body) + assert proxy.respond_to?(:to_path) + assert_equal proxy.to_path, "/path" + end + +end
\ No newline at end of file diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index 7e4ae1ea8d..41312e8661 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -76,7 +76,7 @@ class ReflectionTest < ActiveRecord::TestCase end def test_reflection_klass_for_nested_class_name - reflection = MacroReflection.new(nil, nil, { :class_name => 'MyApplication::Business::Company' }, nil) + reflection = MacroReflection.new(:company, nil, { :class_name => 'MyApplication::Business::Company' }, ActiveRecord::Base) assert_nothing_raised do assert_equal MyApplication::Business::Company, reflection.klass end @@ -304,13 +304,6 @@ class ReflectionTest < ActiveRecord::TestCase assert_equal "category_id", Post.reflect_on_association(:categorizations).foreign_key.to_s end - def test_primary_key_name - assert_deprecated do - assert_equal "author_id", Author.reflect_on_association(:posts).primary_key_name.to_s - assert_equal "category_id", Post.reflect_on_association(:categorizations).primary_key_name.to_s - end - end - private def assert_reflection(klass, association, options) assert reflection = klass.reflect_on_association(association) diff --git a/activerecord/test/cases/relation_scoping_test.rb b/activerecord/test/cases/relation_scoping_test.rb index c215602567..f2d177d834 100644 --- a/activerecord/test/cases/relation_scoping_test.rb +++ b/activerecord/test/cases/relation_scoping_test.rb @@ -11,6 +11,26 @@ require 'models/reference' class RelationScopingTest < ActiveRecord::TestCase fixtures :authors, :developers, :projects, :comments, :posts, :developers_projects + def test_reverse_order + assert_equal Developer.order("id DESC").to_a.reverse, Developer.order("id DESC").reverse_order + end + + def test_reverse_order_with_arel_node + assert_equal Developer.order("id DESC").to_a.reverse, Developer.order(Developer.arel_table[:id].desc).reverse_order + end + + def test_reverse_order_with_multiple_arel_nodes + assert_equal Developer.order("id DESC").order("name DESC").to_a.reverse, Developer.order(Developer.arel_table[:id].desc).order(Developer.arel_table[:name].desc).reverse_order + end + + def test_reverse_order_with_arel_nodes_and_strings + assert_equal Developer.order("id DESC").order("name DESC").to_a.reverse, Developer.order("id DESC").order(Developer.arel_table[:name].desc).reverse_order + end + + def test_double_reverse_order_produces_original_order + assert_equal Developer.order("name DESC"), Developer.order("name DESC").reverse_order.reverse_order + end + def test_scoped_find Developer.where("name = 'David'").scoping do assert_nothing_raised { Developer.find(1) } @@ -312,6 +332,14 @@ class DefaultScopingTest < ActiveRecord::TestCase assert_equal [developers(:david).becomes(ClassMethodDeveloperCalledDavid)], ClassMethodDeveloperCalledDavid.all end + def test_default_scope_as_class_method_referencing_scope + assert_equal [developers(:david).becomes(ClassMethodReferencingScopeDeveloperCalledDavid)], ClassMethodReferencingScopeDeveloperCalledDavid.all + end + + def test_default_scope_as_block_referencing_scope + assert_equal [developers(:david).becomes(LazyBlockReferencingScopeDeveloperCalledDavid)], LazyBlockReferencingScopeDeveloperCalledDavid.all + end + def test_default_scope_with_lambda assert_equal [developers(:david).becomes(LazyLambdaDeveloperCalledDavid)], LazyLambdaDeveloperCalledDavid.all end @@ -456,6 +484,13 @@ class DefaultScopingTest < ActiveRecord::TestCase assert_equal 'Jamis', jamis.name end + # FIXME: I don't know if this is *desired* behavior, but it is *today's* + # behavior. + def test_create_with_empty_hash_will_not_reset + jamis = PoorDeveloperCalledJamis.create_with(:name => 'Aaron').create_with({}).new + assert_equal 'Aaron', jamis.name + end + def test_unscoped_with_named_scope_should_not_have_default_scope assert_equal [DeveloperCalledJamis.find(developers(:poor_jamis).id)], DeveloperCalledJamis.poor @@ -463,7 +498,30 @@ class DefaultScopingTest < ActiveRecord::TestCase assert_equal 10, DeveloperCalledJamis.unscoped.poor.length end + def test_default_scope_select_ignored_by_aggregations + assert_equal DeveloperWithSelect.all.count, DeveloperWithSelect.count + end + + def test_default_scope_select_ignored_by_grouped_aggregations + assert_equal Hash[Developer.all.group_by(&:salary).map { |s, d| [s, d.count] }], + DeveloperWithSelect.group(:salary).count + end + def test_default_scope_order_ignored_by_aggregations assert_equal DeveloperOrderedBySalary.all.count, DeveloperOrderedBySalary.count end + + def test_default_scope_find_last + assert DeveloperOrderedBySalary.count > 1, "need more than one row for test" + + lowest_salary_dev = DeveloperOrderedBySalary.find(developers(:poor_jamis).id) + assert_equal lowest_salary_dev, DeveloperOrderedBySalary.last + end + + def test_default_scope_include_with_count + d = DeveloperWithIncludes.create! + d.audit_logs.create! :message => 'foo' + + assert_equal 1, DeveloperWithIncludes.where(:audit_logs => { :message => 'foo' }).count + end end diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb index 6874bd18f8..b23ead6feb 100644 --- a/activerecord/test/cases/relation_test.rb +++ b/activerecord/test/cases/relation_test.rb @@ -20,7 +20,7 @@ module ActiveRecord end def test_single_values - assert_equal [:limit, :offset, :lock, :readonly, :create_with, :from, :reorder].map(&:to_s).sort, + assert_equal [:limit, :offset, :lock, :readonly, :from, :reorder, :reverse_order].map(&:to_s).sort, Relation::SINGLE_VALUE_METHODS.map(&:to_s).sort end diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index d95acdc39b..0aaa0342b1 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -145,6 +145,18 @@ class RelationTest < ActiveRecord::TestCase assert_equal topics(:first).title, topics.first.title end + + def test_finding_with_arel_order + topics = Topic.order(Topic.arel_table[:id].asc) + assert_equal 4, topics.to_a.size + assert_equal topics(:first).title, topics.first.title + end + + def test_finding_last_with_arel_order + topics = Topic.order(Topic.arel_table[:id].asc) + assert_equal topics(:fourth).title, topics.last.title + end + def test_finding_with_order_concatenated topics = Topic.order('author_name').order('title') assert_equal 4, topics.to_a.size @@ -164,6 +176,13 @@ class RelationTest < ActiveRecord::TestCase assert_equal entrants(:first).name, entrants.first.name end + def test_finding_with_cross_table_order_and_limit + tags = Tag.includes(:taggings). + order("tags.name asc", "taggings.taggable_id asc", "REPLACE('abc', taggings.taggable_type, taggings.taggable_type)"). + limit(1).to_a + assert_equal 1, tags.length + end + def test_finding_with_complex_order_and_limit tags = Tag.includes(:taggings).order("REPLACE('abc', taggings.taggable_type, taggings.taggable_type)").limit(1).to_a assert_equal 1, tags.length diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index e8f2f44189..4adecf8e83 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -230,4 +230,3 @@ 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/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb index 22d4cac422..4445a12e1d 100644 --- a/activerecord/test/cases/timestamp_test.rb +++ b/activerecord/test/cases/timestamp_test.rb @@ -11,35 +11,10 @@ class TimestampTest < ActiveRecord::TestCase def setup @developer = Developer.first + @developer.update_attribute(:updated_at, Time.now.prev_month) @previously_updated_at = @developer.updated_at end - def test_load_infinity_and_beyond - unless current_adapter?(:PostgreSQLAdapter) - return skip("only tested on postgresql") - end - - d = Developer.find_by_sql("select 'infinity'::timestamp as updated_at") - assert d.first.updated_at.infinite?, 'timestamp should be infinite' - - d = Developer.find_by_sql("select '-infinity'::timestamp as updated_at") - time = d.first.updated_at - assert time.infinite?, 'timestamp should be infinite' - assert_operator time, :<, 0 - end - - def test_save_infinity_and_beyond - unless current_adapter?(:PostgreSQLAdapter) - return skip("only tested on postgresql") - end - - d = Developer.create!(:name => 'aaron', :updated_at => 1.0 / 0.0) - assert_equal(1.0 / 0.0, d.updated_at) - - d = Developer.create!(:name => 'aaron', :updated_at => -1.0 / 0.0) - assert_equal(-1.0 / 0.0, d.updated_at) - end - def test_saving_a_changed_record_updates_its_timestamp @developer.name = "Jack Bauer" @developer.save! @@ -66,6 +41,15 @@ class TimestampTest < ActiveRecord::TestCase assert_equal previous_salary, @developer.salary end + def test_touching_a_record_with_default_scope_that_excludes_it_updates_its_timestamp + developer = @developer.becomes(DeveloperCalledJamis) + + developer.touch + assert_not_equal @previously_updated_at, developer.updated_at + developer.reload + assert_not_equal @previously_updated_at, developer.updated_at + end + def test_saving_when_record_timestamps_is_false_doesnt_update_its_timestamp Developer.record_timestamps = false @developer.name = "John Smith" diff --git a/activerecord/test/config.example.yml b/activerecord/test/config.example.yml new file mode 100644 index 0000000000..8c1a45430e --- /dev/null +++ b/activerecord/test/config.example.yml @@ -0,0 +1,136 @@ +default_connection: <%= defined?(JRUBY_VERSION) ? 'jdbcsqlite3' : 'sqlite3' %> + +connections: + jdbcderby: + arunit: activerecord_unittest + arunit2: activerecord_unittest2 + + jdbch2: + arunit: activerecord_unittest + arunit2: activerecord_unittest2 + + jdbchsqldb: + arunit: activerecord_unittest + arunit2: activerecord_unittest2 + + jdbcmysql: + arunit: + username: rails + encoding: utf8 + arunit2: + username: rails + encoding: utf8 + + jdbcpostgresql: + arunit: + username: <%= ENV['user'] || 'rails' %> + arunit2: + username: <%= ENV['user'] || 'rails' %> + + jdbcsqlite3: + arunit: + database: <%= FIXTURES_ROOT %>/fixture_database.sqlite3 + timeout: 5000 + arunit2: + database: <%= FIXTURES_ROOT %>/fixture_database_2.sqlite3 + timeout: 5000 + + db2: + arunit: + host: localhost + username: arunit + password: arunit + database: arunit + arunit2: + host: localhost + username: arunit + password: arunit + database: arunit2 + + firebird: + arunit: + host: localhost + username: rails + password: rails + charset: UTF8 + arunit2: + host: localhost + username: rails + password: rails + charset: UTF8 + + frontbase: + arunit: + host: localhost + username: rails + session_name: unittest-<%= $$ %> + arunit2: + host: localhost + username: rails + session_name: unittest-<%= $$ %> + + mysql: + arunit: + username: rails + encoding: utf8 + arunit2: + username: rails + encoding: utf8 + + mysql2: + arunit: + username: rails + encoding: utf8 + arunit2: + username: rails + encoding: utf8 + + openbase: + arunit: + username: admin + arunit2: + username: admin + + oracle: + arunit: + adapter: oracle_enhanced + database: <%= ENV['ARUNIT_DB_NAME'] || 'orcl' %> + username: <%= ENV['ARUNIT_USER_NAME'] || 'arunit' %> + password: <%= ENV['ARUNIT_PASSWORD'] || 'arunit' %> + emulate_oracle_adapter: true + arunit2: + adapter: oracle_enhanced + database: <%= ENV['ARUNIT_DB_NAME'] || 'orcl' %> + username: <%= ENV['ARUNIT2_USER_NAME'] || 'arunit2' %> + password: <%= ENV['ARUNIT2_PASSWORD'] || 'arunit2' %> + emulate_oracle_adapter: true + + postgresql: + arunit: + min_messages: warning + arunit2: + min_messages: warning + + sqlite3: + arunit: + database: <%= FIXTURES_ROOT %>/fixture_database.sqlite3 + timeout: 5000 + arunit2: + database: <%= FIXTURES_ROOT %>/fixture_database_2.sqlite3 + timeout: 5000 + + sqlite3_mem: + arunit: + adapter: sqlite3 + database: ':memory:' + arunit2: + adapter: sqlite3 + database: ':memory:' + + sybase: + arunit: + host: database_ASE + username: sa + arunit2: + host: database_ASE + username: sa diff --git a/activerecord/test/connections/jdbc_jdbcderby/connection.rb b/activerecord/test/connections/jdbc_jdbcderby/connection.rb deleted file mode 100644 index 222ef5db38..0000000000 --- a/activerecord/test/connections/jdbc_jdbcderby/connection.rb +++ /dev/null @@ -1,18 +0,0 @@ -print "Using Derby via JRuby, activerecord-jdbc-adapter and activerecord-jdbcderby-adapter\n" -require_dependency 'models/course' -require 'logger' -ActiveRecord::Base.logger = Logger.new("debug.log") - -ActiveRecord::Base.configurations = { - 'arunit' => { - :adapter => 'jdbcderby', - :database => 'activerecord_unittest' - }, - 'arunit2' => { - :adapter => 'jdbcderby', - :database => 'activerecord_unittest2' - } -} - -ActiveRecord::Base.establish_connection 'arunit' -Course.establish_connection 'arunit2' diff --git a/activerecord/test/connections/jdbc_jdbch2/connection.rb b/activerecord/test/connections/jdbc_jdbch2/connection.rb deleted file mode 100644 index 9d2875e8e7..0000000000 --- a/activerecord/test/connections/jdbc_jdbch2/connection.rb +++ /dev/null @@ -1,18 +0,0 @@ -print "Using H2 via JRuby, activerecord-jdbc-adapter and activerecord-jdbch2-adapter\n" -require_dependency 'models/course' -require 'logger' -ActiveRecord::Base.logger = Logger.new("debug.log") - -ActiveRecord::Base.configurations = { - 'arunit' => { - :adapter => 'jdbch2', - :database => 'activerecord_unittest' - }, - 'arunit2' => { - :adapter => 'jdbch2', - :database => 'activerecord_unittest2' - } -} - -ActiveRecord::Base.establish_connection 'arunit' -Course.establish_connection 'arunit2' diff --git a/activerecord/test/connections/jdbc_jdbchsqldb/connection.rb b/activerecord/test/connections/jdbc_jdbchsqldb/connection.rb deleted file mode 100644 index fa943c2c76..0000000000 --- a/activerecord/test/connections/jdbc_jdbchsqldb/connection.rb +++ /dev/null @@ -1,18 +0,0 @@ -print "Using HSQLDB via JRuby, activerecord-jdbc-adapter and activerecord-jdbchsqldb-adapter\n" -require_dependency 'models/course' -require 'logger' -ActiveRecord::Base.logger = Logger.new("debug.log") - -ActiveRecord::Base.configurations = { - 'arunit' => { - :adapter => 'jdbchsqldb', - :database => 'activerecord_unittest' - }, - 'arunit2' => { - :adapter => 'jdbchsqldb', - :database => 'activerecord_unittest2' - } -} - -ActiveRecord::Base.establish_connection 'arunit' -Course.establish_connection 'arunit2' diff --git a/activerecord/test/connections/jdbc_jdbcmysql/connection.rb b/activerecord/test/connections/jdbc_jdbcmysql/connection.rb deleted file mode 100644 index e2517a50eb..0000000000 --- a/activerecord/test/connections/jdbc_jdbcmysql/connection.rb +++ /dev/null @@ -1,26 +0,0 @@ -print "Using MySQL via JRuby, activerecord-jdbc-adapter and activerecord-jdbcmysql-adapter\n" -require_dependency 'models/course' -require 'logger' - -ActiveRecord::Base.logger = Logger.new("debug.log") - -# GRANT ALL PRIVILEGES ON activerecord_unittest.* to 'rails'@'localhost'; -# GRANT ALL PRIVILEGES ON activerecord_unittest2.* to 'rails'@'localhost'; - -ActiveRecord::Base.configurations = { - 'arunit' => { - :adapter => 'jdbcmysql', - :username => 'rails', - :encoding => 'utf8', - :database => 'activerecord_unittest', - }, - 'arunit2' => { - :adapter => 'jdbcmysql', - :username => 'rails', - :database => 'activerecord_unittest2' - } -} - -ActiveRecord::Base.establish_connection 'arunit' -Course.establish_connection 'arunit2' - diff --git a/activerecord/test/connections/jdbc_jdbcpostgresql/connection.rb b/activerecord/test/connections/jdbc_jdbcpostgresql/connection.rb deleted file mode 100644 index 0685da4433..0000000000 --- a/activerecord/test/connections/jdbc_jdbcpostgresql/connection.rb +++ /dev/null @@ -1,26 +0,0 @@ -print "Using Postgrsql via JRuby, activerecord-jdbc-adapter and activerecord-postgresql-adapter\n" -require_dependency 'models/course' -require 'logger' - -ActiveRecord::Base.logger = Logger.new("debug.log") - -# createuser rails --createdb --no-superuser --no-createrole -# createdb -O rails activerecord_unittest -# createdb -O rails activerecord_unittest2 - -ActiveRecord::Base.configurations = { - 'arunit' => { - :adapter => 'jdbcpostgresql', - :username => ENV['USER'] || 'rails', - :database => 'activerecord_unittest' - }, - 'arunit2' => { - :adapter => 'jdbcpostgresql', - :username => ENV['USER'] || 'rails', - :database => 'activerecord_unittest2' - } -} - -ActiveRecord::Base.establish_connection 'arunit' -Course.establish_connection 'arunit2' - diff --git a/activerecord/test/connections/jdbc_jdbcsqlite3/connection.rb b/activerecord/test/connections/jdbc_jdbcsqlite3/connection.rb deleted file mode 100644 index 26d4676ff3..0000000000 --- a/activerecord/test/connections/jdbc_jdbcsqlite3/connection.rb +++ /dev/null @@ -1,25 +0,0 @@ -print "Using SQLite3 via JRuby, activerecord-jdbc-adapter and activerecord-jdbcsqlite3-adapter\n" -require_dependency 'models/course' -require 'logger' -ActiveRecord::Base.logger = Logger.new("debug.log") - -class SqliteError < StandardError -end - -BASE_DIR = FIXTURES_ROOT -sqlite_test_db = "#{BASE_DIR}/fixture_database.sqlite3" -sqlite_test_db2 = "#{BASE_DIR}/fixture_database_2.sqlite3" - -def make_connection(clazz, db_file) - ActiveRecord::Base.configurations = { clazz.name => { :adapter => 'jdbcsqlite3', :database => db_file, :timeout => 5000 } } - unless File.exist?(db_file) - puts "SQLite3 database not found at #{db_file}. Rebuilding it." - sqlite_command = %Q{sqlite3 "#{db_file}" "create table a (a integer); drop table a;"} - puts "Executing '#{sqlite_command}'" - raise SqliteError.new("Seems that there is no sqlite3 executable available") unless system(sqlite_command) - end - clazz.establish_connection(clazz.name) -end - -make_connection(ActiveRecord::Base, sqlite_test_db) -make_connection(Course, sqlite_test_db2) diff --git a/activerecord/test/connections/native_db2/connection.rb b/activerecord/test/connections/native_db2/connection.rb deleted file mode 100644 index 324315d2c8..0000000000 --- a/activerecord/test/connections/native_db2/connection.rb +++ /dev/null @@ -1,25 +0,0 @@ -print "Using native DB2\n" -require_dependency 'models/course' -require 'logger' - -ActiveRecord::Base.logger = Logger.new("debug.log") - -ActiveRecord::Base.configurations = { - 'arunit' => { - :adapter => 'db2', - :host => 'localhost', - :username => 'arunit', - :password => 'arunit', - :database => 'arunit' - }, - 'arunit2' => { - :adapter => 'db2', - :host => 'localhost', - :username => 'arunit', - :password => 'arunit', - :database => 'arunit2' - } -} - -ActiveRecord::Base.establish_connection 'arunit' -Course.establish_connection 'arunit2' diff --git a/activerecord/test/connections/native_firebird/connection.rb b/activerecord/test/connections/native_firebird/connection.rb deleted file mode 100644 index 67a936ca97..0000000000 --- a/activerecord/test/connections/native_firebird/connection.rb +++ /dev/null @@ -1,26 +0,0 @@ -print "Using native Firebird\n" -require_dependency 'models/course' -require 'logger' - -ActiveRecord::Base.logger = Logger.new("debug.log") - -ActiveRecord::Base.configurations = { - 'arunit' => { - :adapter => 'firebird', - :host => 'localhost', - :username => 'rails', - :password => 'rails', - :database => 'activerecord_unittest', - :charset => 'UTF8' - }, - 'arunit2' => { - :adapter => 'firebird', - :host => 'localhost', - :username => 'rails', - :password => 'rails', - :database => 'activerecord_unittest2' - } -} - -ActiveRecord::Base.establish_connection 'arunit' -Course.establish_connection 'arunit2' diff --git a/activerecord/test/connections/native_frontbase/connection.rb b/activerecord/test/connections/native_frontbase/connection.rb deleted file mode 100644 index c01d864a8b..0000000000 --- a/activerecord/test/connections/native_frontbase/connection.rb +++ /dev/null @@ -1,27 +0,0 @@ -puts 'Using native Frontbase' -require_dependency 'models/course' -require 'logger' - -ActiveRecord::Base.logger = Logger.new("debug.log") - -ActiveRecord::Base.configurations = { - 'arunit' => { - :adapter => 'frontbase', - :host => 'localhost', - :username => 'rails', - :password => '', - :database => 'activerecord_unittest', - :session_name => "unittest-#{$$}" - }, - 'arunit2' => { - :adapter => 'frontbase', - :host => 'localhost', - :username => 'rails', - :password => '', - :database => 'activerecord_unittest2', - :session_name => "unittest-#{$$}" - } -} - -ActiveRecord::Base.establish_connection 'arunit' -Course.establish_connection 'arunit2' diff --git a/activerecord/test/connections/native_mysql/connection.rb b/activerecord/test/connections/native_mysql/connection.rb deleted file mode 100644 index 140e06d631..0000000000 --- a/activerecord/test/connections/native_mysql/connection.rb +++ /dev/null @@ -1,25 +0,0 @@ -print "Using native MySQL\n" -require_dependency 'models/course' -require 'logger' - -ActiveRecord::Base.logger = Logger.new("debug.log") - -# GRANT ALL PRIVILEGES ON activerecord_unittest.* to 'rails'@'localhost'; -# GRANT ALL PRIVILEGES ON activerecord_unittest2.* to 'rails'@'localhost'; - -ActiveRecord::Base.configurations = { - 'arunit' => { - :adapter => 'mysql', - :username => 'rails', - :encoding => 'utf8', - :database => 'activerecord_unittest', - }, - 'arunit2' => { - :adapter => 'mysql', - :username => 'rails', - :database => 'activerecord_unittest2' - } -} - -ActiveRecord::Base.establish_connection 'arunit' -Course.establish_connection 'arunit2' diff --git a/activerecord/test/connections/native_mysql2/connection.rb b/activerecord/test/connections/native_mysql2/connection.rb deleted file mode 100644 index c6f198b1ac..0000000000 --- a/activerecord/test/connections/native_mysql2/connection.rb +++ /dev/null @@ -1,25 +0,0 @@ -print "Using native Mysql2\n" -require_dependency 'models/course' -require 'logger' - -ActiveRecord::Base.logger = Logger.new("debug.log") - -# GRANT ALL PRIVILEGES ON activerecord_unittest.* to 'rails'@'localhost'; -# GRANT ALL PRIVILEGES ON activerecord_unittest2.* to 'rails'@'localhost'; - -ActiveRecord::Base.configurations = { - 'arunit' => { - :adapter => 'mysql2', - :username => 'rails', - :encoding => 'utf8', - :database => 'activerecord_unittest', - }, - 'arunit2' => { - :adapter => 'mysql2', - :username => 'rails', - :database => 'activerecord_unittest2' - } -} - -ActiveRecord::Base.establish_connection 'arunit' -Course.establish_connection 'arunit2' diff --git a/activerecord/test/connections/native_openbase/connection.rb b/activerecord/test/connections/native_openbase/connection.rb deleted file mode 100644 index 655cb9ca26..0000000000 --- a/activerecord/test/connections/native_openbase/connection.rb +++ /dev/null @@ -1,21 +0,0 @@ -print "Using native OpenBase\n" -require_dependency 'models/course' -require 'logger' - -ActiveRecord::Base.logger = Logger.new("debug.log") - -ActiveRecord::Base.configurations = { - 'arunit' => { - :adapter => 'openbase', - :username => 'admin', - :database => 'activerecord_unittest', - }, - 'arunit2' => { - :adapter => 'openbase', - :username => 'admin', - :database => 'activerecord_unittest2' - } -} - -ActiveRecord::Base.establish_connection 'arunit' -Course.establish_connection 'arunit2' diff --git a/activerecord/test/connections/native_oracle/connection.rb b/activerecord/test/connections/native_oracle/connection.rb deleted file mode 100644 index 99f921879c..0000000000 --- a/activerecord/test/connections/native_oracle/connection.rb +++ /dev/null @@ -1,35 +0,0 @@ -# uses oracle_enhanced adapter in ENV['ORACLE_ENHANCED_PATH'] or from github.com/rsim/oracle-enhanced.git -require 'active_record/connection_adapters/oracle_enhanced_adapter' - -# otherwise failed with silence_warnings method missing exception -require 'active_support/core_ext/kernel/reporting' - -print "Using Oracle\n" -require_dependency 'models/course' -require 'logger' - -ActiveRecord::Base.logger = Logger.new("debug.log") - -# Set these to your database connection strings -ENV['ARUNIT_DB_NAME'] ||= 'orcl' - -ActiveRecord::Base.configurations = { - 'arunit' => { - :adapter => 'oracle_enhanced', - :database => ENV['ARUNIT_DB_NAME'], - :username => 'arunit', - :password => 'arunit', - :emulate_oracle_adapter => true - }, - 'arunit2' => { - :adapter => 'oracle_enhanced', - :database => ENV['ARUNIT_DB_NAME'], - :username => 'arunit2', - :password => 'arunit2', - :emulate_oracle_adapter => true - } -} - -ActiveRecord::Base.establish_connection 'arunit' -Course.establish_connection 'arunit2' - diff --git a/activerecord/test/connections/native_postgresql/connection.rb b/activerecord/test/connections/native_postgresql/connection.rb deleted file mode 100644 index 3b5ff90003..0000000000 --- a/activerecord/test/connections/native_postgresql/connection.rb +++ /dev/null @@ -1,21 +0,0 @@ -print "Using native PostgreSQL\n" -require_dependency 'models/course' -require 'logger' - -ActiveRecord::Base.logger = Logger.new("debug.log") - -ActiveRecord::Base.configurations = { - 'arunit' => { - :adapter => 'postgresql', - :database => 'activerecord_unittest', - :min_messages => 'warning' - }, - 'arunit2' => { - :adapter => 'postgresql', - :database => 'activerecord_unittest2', - :min_messages => 'warning' - } -} - -ActiveRecord::Base.establish_connection 'arunit' -Course.establish_connection 'arunit2' diff --git a/activerecord/test/connections/native_sqlite3/connection.rb b/activerecord/test/connections/native_sqlite3/connection.rb deleted file mode 100644 index c2aff5551f..0000000000 --- a/activerecord/test/connections/native_sqlite3/connection.rb +++ /dev/null @@ -1,16 +0,0 @@ -print "Using native SQLite3\n" -require_dependency 'models/course' -require 'logger' -ActiveRecord::Base.logger = Logger.new("debug.log") - -BASE_DIR = FIXTURES_ROOT -sqlite_test_db = "#{BASE_DIR}/fixture_database.sqlite3" -sqlite_test_db2 = "#{BASE_DIR}/fixture_database_2.sqlite3" - -def make_connection(clazz, db_file) - ActiveRecord::Base.configurations = { clazz.name => { :adapter => 'sqlite3', :database => db_file, :timeout => 5000 } } - clazz.establish_connection(clazz.name) -end - -make_connection(ActiveRecord::Base, sqlite_test_db) -make_connection(Course, sqlite_test_db2) diff --git a/activerecord/test/connections/native_sqlite3_mem/connection.rb b/activerecord/test/connections/native_sqlite3_mem/connection.rb deleted file mode 100644 index 14e10900d1..0000000000 --- a/activerecord/test/connections/native_sqlite3_mem/connection.rb +++ /dev/null @@ -1,19 +0,0 @@ -# This file connects to an in-memory SQLite3 database, which is a very fast way to run the tests. -# The downside is that disconnect from the database results in the database effectively being -# wiped. For this reason, pooled_connections_test.rb is disabled when using an in-memory database. - -print "Using native SQLite3 (in memory)\n" -require_dependency 'models/course' -require 'logger' -ActiveRecord::Base.logger = Logger.new("debug.log") - -class SqliteError < StandardError -end - -def make_connection(clazz) - ActiveRecord::Base.configurations = { clazz.name => { :adapter => 'sqlite3', :database => ':memory:' } } - clazz.establish_connection(clazz.name) -end - -make_connection(ActiveRecord::Base) -make_connection(Course) diff --git a/activerecord/test/connections/native_sybase/connection.rb b/activerecord/test/connections/native_sybase/connection.rb deleted file mode 100644 index 3282d26922..0000000000 --- a/activerecord/test/connections/native_sybase/connection.rb +++ /dev/null @@ -1,23 +0,0 @@ -print "Using native Sybase Open Client\n" -require_dependency 'models/course' -require 'logger' - -ActiveRecord::Base.logger = Logger.new("debug.log") - -ActiveRecord::Base.configurations = { - 'arunit' => { - :adapter => 'sybase', - :host => 'database_ASE', - :username => 'sa', - :database => 'activerecord_unittest' - }, - 'arunit2' => { - :adapter => 'sybase', - :host => 'database_ASE', - :username => 'sa', - :database => 'activerecord_unittest2' - } -} - -ActiveRecord::Base.establish_connection 'arunit' -Course.establish_connection 'arunit2' diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb index e0cbc44265..23db5650d4 100644 --- a/activerecord/test/models/author.rb +++ b/activerecord/test/models/author.rb @@ -138,6 +138,9 @@ class Author < ActiveRecord::Base has_many :misc_post_first_blue_tags_2, :through => :posts, :source => :first_blue_tags_2, :conditions => { :posts => { :title => ['misc post by bob', 'misc post by mary'] } } + has_many :posts_with_default_include, :class_name => 'PostWithDefaultInclude' + has_many :comments_on_posts_with_default_include, :through => :posts_with_default_include, :source => :comments + scope :relation_include_posts, includes(:posts) scope :relation_include_tags, includes(:tags) diff --git a/activerecord/test/models/bulb.rb b/activerecord/test/models/bulb.rb index 0dcc8d5970..efb98b66e7 100644 --- a/activerecord/test/models/bulb.rb +++ b/activerecord/test/models/bulb.rb @@ -4,13 +4,18 @@ class Bulb < ActiveRecord::Base attr_protected :car_id, :frickinawesome - attr_reader :scope_after_initialize + attr_reader :scope_after_initialize, :attributes_after_initialize after_initialize :record_scope_after_initialize def record_scope_after_initialize @scope_after_initialize = self.class.scoped end + after_initialize :record_attributes_after_initialize + def record_attributes_after_initialize + @attributes_after_initialize = attributes.dup + end + def color=(color) self[:color] = color.upcase + "!" end @@ -28,4 +33,4 @@ class Bulb < ActiveRecord::Base end class CustomBulb < Bulb -end
\ No newline at end of file +end diff --git a/activerecord/test/models/comment.rb b/activerecord/test/models/comment.rb index 2a4c37089a..43650c0427 100644 --- a/activerecord/test/models/comment.rb +++ b/activerecord/test/models/comment.rb @@ -6,7 +6,8 @@ class Comment < ActiveRecord::Base scope :for_first_author, :joins => :post, :conditions => { "posts.author_id" => 1 } - + scope :created + belongs_to :post, :counter_cache => true has_many :ratings diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb index e0b30efd51..c1f7a4171a 100644 --- a/activerecord/test/models/company.rb +++ b/activerecord/test/models/company.rb @@ -124,6 +124,18 @@ class Client < Company has_many :accounts, :through => :firm belongs_to :account + class RaisedOnSave < RuntimeError; end + attr_accessor :raise_on_save + before_save do + raise RaisedOnSave if raise_on_save + end + + class RaisedOnDestroy < RuntimeError; end + attr_accessor :raise_on_destroy + before_destroy do + raise RaisedOnDestroy if raise_on_destroy + end + # Record destruction so we can test whether firm.clients.clear has # is calling client.destroy, deleting from the database, or setting # foreign keys to NULL. diff --git a/activerecord/test/models/contract.rb b/activerecord/test/models/contract.rb index 94fd48e12a..2cf5aa7a85 100644 --- a/activerecord/test/models/contract.rb +++ b/activerecord/test/models/contract.rb @@ -1,4 +1,19 @@ class Contract < ActiveRecord::Base belongs_to :company belongs_to :developer + + before_save :hi + after_save :bye + + attr_accessor :hi_count, :bye_count + + def hi + @hi_count ||= 0 + @hi_count += 1 + end + + def bye + @bye_count ||= 0 + @bye_count += 1 + end end diff --git a/activerecord/test/models/developer.rb b/activerecord/test/models/developer.rb index 152f804e16..f182a7fa97 100644 --- a/activerecord/test/models/developer.rb +++ b/activerecord/test/models/developer.rb @@ -86,6 +86,17 @@ class DeveloperWithBeforeDestroyRaise < ActiveRecord::Base end end +class DeveloperWithSelect < ActiveRecord::Base + self.table_name = 'developers' + default_scope select('name') +end + +class DeveloperWithIncludes < ActiveRecord::Base + self.table_name = 'developers' + has_many :audit_logs, :foreign_key => :developer_id + default_scope includes(:audit_logs) +end + class DeveloperOrderedBySalary < ActiveRecord::Base self.table_name = 'developers' default_scope :order => 'salary DESC' @@ -127,6 +138,21 @@ class ClassMethodDeveloperCalledDavid < ActiveRecord::Base end end +class ClassMethodReferencingScopeDeveloperCalledDavid < ActiveRecord::Base + self.table_name = 'developers' + scope :david, where(:name => 'David') + + def self.default_scope + david + end +end + +class LazyBlockReferencingScopeDeveloperCalledDavid < ActiveRecord::Base + self.table_name = 'developers' + scope :david, where(:name => 'David') + default_scope { david } +end + class DeveloperCalledJamis < ActiveRecord::Base self.table_name = 'developers' @@ -165,4 +191,39 @@ class ModuleIncludedPoorDeveloperCalledJamis < DeveloperCalledJamis include SalaryDefaultScope end +class EagerDeveloperWithDefaultScope < ActiveRecord::Base + self.table_name = 'developers' + has_and_belongs_to_many :projects, :foreign_key => 'developer_id', :join_table => 'developers_projects', :order => 'projects.id' + + default_scope includes(:projects) +end +class EagerDeveloperWithClassMethodDefaultScope < ActiveRecord::Base + self.table_name = 'developers' + has_and_belongs_to_many :projects, :foreign_key => 'developer_id', :join_table => 'developers_projects', :order => 'projects.id' + + def self.default_scope + includes(:projects) + end +end + +class EagerDeveloperWithLambdaDefaultScope < ActiveRecord::Base + self.table_name = 'developers' + has_and_belongs_to_many :projects, :foreign_key => 'developer_id', :join_table => 'developers_projects', :order => 'projects.id' + + default_scope lambda { includes(:projects) } +end + +class EagerDeveloperWithBlockDefaultScope < ActiveRecord::Base + self.table_name = 'developers' + has_and_belongs_to_many :projects, :foreign_key => 'developer_id', :join_table => 'developers_projects', :order => 'projects.id' + + default_scope { includes(:projects) } +end + +class EagerDeveloperWithCallableDefaultScope < ActiveRecord::Base + self.table_name = 'developers' + has_and_belongs_to_many :projects, :foreign_key => 'developer_id', :join_table => 'developers_projects', :order => 'projects.id' + + default_scope OpenStruct.new(:call => includes(:projects)) +end diff --git a/activerecord/test/models/person.rb b/activerecord/test/models/person.rb index a58c9bf572..967a3625aa 100644 --- a/activerecord/test/models/person.rb +++ b/activerecord/test/models/person.rb @@ -59,8 +59,9 @@ class LoosePerson < ActiveRecord::Base has_one :best_friend, :class_name => 'LoosePerson', :foreign_key => :best_friend_id belongs_to :best_friend_of, :class_name => 'LoosePerson', :foreign_key => :best_friend_of_id - has_many :best_friends, :class_name => 'LoosePerson', :foreign_key => :best_friend_id + + accepts_nested_attributes_for :best_friend, :best_friend_of, :best_friends end class LooseDescendant < LoosePerson; end @@ -70,11 +71,14 @@ class TightPerson < ActiveRecord::Base attr_accessible :first_name, :gender attr_accessible :first_name, :gender, :comments, :as => :admin + attr_accessible :best_friend_attributes, :best_friend_of_attributes, :best_friends_attributes + attr_accessible :best_friend_attributes, :best_friend_of_attributes, :best_friends_attributes, :as => :admin has_one :best_friend, :class_name => 'TightPerson', :foreign_key => :best_friend_id belongs_to :best_friend_of, :class_name => 'TightPerson', :foreign_key => :best_friend_of_id - has_many :best_friends, :class_name => 'TightPerson', :foreign_key => :best_friend_id + + accepts_nested_attributes_for :best_friend, :best_friend_of, :best_friends end class TightDescendant < TightPerson; end
\ No newline at end of file diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index 80296032bb..affa37b02d 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -36,6 +36,10 @@ class Post < ActiveRecord::Base def find_most_recent find(:first, :order => "id DESC") end + + def newest + created.last + end end has_many :author_favorites, :through => :author @@ -162,3 +166,9 @@ class FirstPost < ActiveRecord::Base has_many :comments, :foreign_key => :post_id has_one :comment, :foreign_key => :post_id end + +class PostWithDefaultInclude < ActiveRecord::Base + self.table_name = 'posts' + default_scope includes(:comments) + has_many :comments, :foreign_key => :post_id +end
\ No newline at end of file diff --git a/activerecord/test/support/config.rb b/activerecord/test/support/config.rb new file mode 100644 index 0000000000..6d123688a3 --- /dev/null +++ b/activerecord/test/support/config.rb @@ -0,0 +1,43 @@ +require 'yaml' +require 'erubis' +require 'fileutils' +require 'pathname' + +module ARTest + class << self + def config + @config ||= read_config + end + + private + + def config_file + Pathname.new(ENV['ARCONFIG'] || TEST_ROOT + '/config.yml') + end + + def read_config + unless config_file.exist? + FileUtils.cp TEST_ROOT + '/config.example.yml', config_file + end + + erb = Erubis::Eruby.new(config_file.read) + expand_config(YAML.parse(erb.result(binding)).transform) + end + + def expand_config(config) + config['connections'].each do |adapter, connection| + dbs = [['arunit', 'activerecord_unittest'], ['arunit2', 'activerecord_unittest2']] + dbs.each do |name, dbname| + unless connection[name].is_a?(Hash) + connection[name] = { 'database' => connection[name] } + end + + connection[name]['database'] ||= dbname + connection[name]['adapter'] ||= adapter + end + end + + config + end + end +end diff --git a/activerecord/test/support/connection.rb b/activerecord/test/support/connection.rb new file mode 100644 index 0000000000..a39794fa39 --- /dev/null +++ b/activerecord/test/support/connection.rb @@ -0,0 +1,20 @@ +require 'logger' +require_dependency 'models/course' + +module ARTest + def self.connection_name + ENV['ARCONN'] || config['default_connection'] + end + + def self.connection_config + config['connections'][connection_name] + end + + def self.connect + puts "Using #{connection_name} with Identity Map #{ActiveRecord::IdentityMap.enabled? ? 'on' : 'off'}" + ActiveRecord::Base.logger = Logger.new("debug.log") + ActiveRecord::Base.configurations = connection_config + ActiveRecord::Base.establish_connection 'arunit' + Course.establish_connection 'arunit2' + end +end |