diff options
Diffstat (limited to 'activerecord')
243 files changed, 7503 insertions, 3832 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index b04c54ce7f..c5ef39b9d2 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,5 +1,289 @@ ## Rails 4.0.0 (unreleased) ## +* Fix `reset_counters` when there are multiple `belongs_to` association with the + same foreign key and one of them have a counter cache. + Fixes #5200. + + *Dave Desrochers* + +* `serialized_attributes` and `_attr_readonly` become class method only. Instance reader methods are deprecated. + + *kennyj* + +* Round usec when comparing timestamp attributes in the dirty tracking. + Fixes #6975. + + *kennyj* + +* Use inversed parent for first and last child of has_many association. + + *Ravil Bayramgalin* + +* Fix Column.microseconds and Column.fast_string_to_date to avoid converting + timestamp seconds to a float, since it occasionally results in inaccuracies + with microsecond-precision times. Fixes #7352. + + *Ari Pollak* + +* Raise `ArgumentError` if list of attributes to change is empty in `update_all`. + + *Roman Shatsov* + +* Fix AR#create to return an unsaved record when AR::RecordInvalid is + raised. Fixes #3217. + + *Dave Yeu* + +* Fixed table name prefix that is generated in engines for namespaced models + *Wojciech Wnętrzak* + +* Make sure `:environment` task is executed before `db:schema:load` or `db:structure:load` + Fixes #4772. + + *Seamus Abshere* + +* Allow Relation#merge to take a proc. + + This was requested by DHH to allow creating of one's own custom + association macros. + + For example: + + module Commentable + def has_many_comments(extra) + has_many :comments, -> { where(:foo).merge(extra) } + end + end + + class Post < ActiveRecord::Base + extend Commentable + has_many_comments -> { where(:bar) } + end + + *Jon Leighton* + +* Add CollectionProxy#scope + + This can be used to get a Relation from an association. + + Previously we had a #scoped method, but we're deprecating that for + AR::Base, so it doesn't make sense to have it here. + + This was requested by DHH, to facilitate code like this: + + Project.scope.order('created_at DESC').page(current_page).tagged_with(@tag).limit(5).scoping do + @topics = @project.topics.scope + @todolists = @project.todolists.scope + @attachments = @project.attachments.scope + @documents = @project.documents.scope + end + + *Jon Leighton* + +* Add `Relation#load` + + This method explicitly loads the records and then returns `self`. + + Rather than deciding between "do I want an array or a relation?", + most people are actually asking themselves "do I want to eager load + or lazy load?" Therefore, this method provides a way to explicitly + eager-load without having to switch from a `Relation` to an array. + + Example: + + @posts = Post.where(published: true).load + + *Jon Leighton* + +* `Model.all` now returns an `ActiveRecord::Relation`, rather than an + array of records. Use ``Relation#to_a` if you really want an array. + + In some specific cases, this may cause breakage when upgrading. + However in most cases the `ActiveRecord::Relation` will just act as a + lazy-loaded array and there will be no problems. + + Note that calling `Model.all` with options (e.g. + `Model.all(conditions: '...')` was already deprecated, but it will + still return an array in order to make the transition easier. + + `Model.scoped` is deprecated in favour of `Model.all`. + + `Relation#all` still returns an array, but is deprecated (since it + would serve no purpose if we made it return a `Relation`). + + *Jon Leighton* + +* `:finder_sql` and `:counter_sql` options on collection associations + are deprecated. Please transition to using scopes. + + *Jon Leighton* + +* `:insert_sql` and `:delete_sql` options on `has_and_belongs_to_many` + associations are deprecated. Please transition to using `has_many + :through` + + *Jon Leighton* + +* The migration generator now creates a join table with (commented) indexes every time + the migration name contains the word `join_table`: + + rails g migration create_join_table_for_artists_and_musics artist_id:index music_id + + *Aleksey Magusev* + +* Add `add_reference` and `remove_reference` schema statements. Aliases, `add_belongs_to` + and `remove_belongs_to` are acceptable. References are reversible. + Examples: + + # Create a user_id column + add_reference(:products, :user) + # Create a supplier_id, supplier_type columns and appropriate index + add_reference(:products, :supplier, polymorphic: true, index: true) + # Remove polymorphic reference + remove_reference(:products, :supplier, polymorphic: true) + + *Aleksey Magusev* + +* Add `:default` and `:null` options to `column_exists?`. + + column_exists?(:testings, :taggable_id, :integer, null: false) + column_exists?(:testings, :taggable_type, :string, default: 'Photo') + + *Aleksey Magusev* + +* `ActiveRecord::Relation#inspect` now makes it clear that you are + dealing with a `Relation` object rather than an array:. + + User.where(:age => 30).inspect + # => <ActiveRecord::Relation [#<User ...>, #<User ...>, ...]> + + User.where(:age => 30).to_a.inspect + # => [#<User ...>, #<User ...>] + + The number of records displayed will be limited to 10. + + *Brian Cardarella, Jon Leighton & Damien Mathieu* + +* Add `collation` and `ctype` support to PostgreSQL. These are available for PostgreSQL 8.4 or later. + Example: + + development: + adapter: postgresql + host: localhost + database: rails_development + username: foo + password: bar + encoding: UTF8 + collation: ja_JP.UTF8 + ctype: ja_JP.UTF8 + + *kennyj* + +* Changed validates_presence_of on an association so that children objects + do not validate as being present if they are marked for destruction. This + prevents you from saving the parent successfully and thus putting the parent + in an invalid state. + + *Nick Monje & Brent Wheeldon* + +* `FinderMethods#exists?` now returns `false` with the `false` argument. + + *Egor Lynko* + +* Added support for specifying the precision of a timestamp in the postgresql + adapter. So, instead of having to incorrectly specify the precision using the + `:limit` option, you may use `:precision`, as intended. For example, in a migration: + + def change + create_table :foobars do |t| + t.timestamps :precision => 0 + end + end + + *Tony Schneider* + +* Allow `ActiveRecord::Relation#pluck` to accept multiple columns. Returns an + array of arrays containing the typecasted values: + + Person.pluck(:id, :name) + # SELECT people.id, people.name FROM people + # [[1, 'David'], [2, 'Jeremy'], [3, 'Jose']] + + *Jeroen van Ingen & Carlos Antonio da Silva* + +* Improve the derivation of HABTM join table name to take account of nesting. + It now takes the table names of the two models, sorts them lexically and + then joins them, stripping any common prefix from the second table name. + + Some examples: + + Top level models (Category <=> Product) + Old: categories_products + New: categories_products + + Top level models with a global table_name_prefix (Category <=> Product) + Old: site_categories_products + New: site_categories_products + + Nested models in a module without a table_name_prefix method (Admin::Category <=> Admin::Product) + Old: categories_products + New: categories_products + + Nested models in a module with a table_name_prefix method (Admin::Category <=> Admin::Product) + Old: categories_products + New: admin_categories_products + + Nested models in a parent model (Catalog::Category <=> Catalog::Product) + Old: categories_products + New: catalog_categories_products + + Nested models in different parent models (Catalog::Category <=> Content::Page) + Old: categories_pages + New: catalog_categories_content_pages + + *Andrew White* + +* Move HABTM validity checks to `ActiveRecord::Reflection`. One side effect of + this is to move when the exceptions are raised from the point of declaration + to when the association is built. This is consistant with other association + validity checks. + + *Andrew White* + +* Added `stored_attributes` hash which contains the attributes stored using + `ActiveRecord::Store`. This allows you to retrieve the list of attributes + you've defined. + + class User < ActiveRecord::Base + store :settings, accessors: [:color, :homepage] + end + + User.stored_attributes[:settings] # [:color, :homepage] + + *Joost Baaij & Carlos Antonio da Silva* + +* PostgreSQL default log level is now 'warning', to bypass the noisy notice + messages. You can change the log level using the `min_messages` option + available in your config/database.yml. + + *kennyj* + +* Add uuid datatype support to PostgreSQL adapter. *Konstantin Shabanov* + +* `update_attribute` has been removed. Use `update_columns` if + you want to bypass mass-assignment protection, validations, callbacks, + and touching of updated_at. Otherwise please use `update_attributes`. + + *Steve Klabnik* + +* Added `ActiveRecord::Migration.check_pending!` that raises an error if + migrations are pending. *Richard Schneeman* + +* Added `#destroy!` which acts like `#destroy` but will raise an + `ActiveRecord::RecordNotDestroyed` exception instead of returning `false`. + + *Marc-André Lafortune* + * Allow blocks for `count` with `ActiveRecord::Relation`, to work similar as `Array#count`: @@ -35,7 +319,7 @@ `where(...).first_or_create!` The implementation of the deprecated dynamic finders has been moved - to the `active_record_deprecated_finders` gem. See below for details. + to the `activerecord-deprecated_finders` gem. See below for details. *Jon Leighton* @@ -51,13 +335,12 @@ Note that as an interim step, it is possible to rewrite the above as: - Post.scoped(:where => { :comments_count => 10 }, :limit => 5) + Post.all.merge(:where => { :comments_count => 10 }, :limit => 5) This could save you a lot of work if there is a lot of old-style finder usage in your application. - Calling `Post.scoped(options)` is a shortcut for - `Post.scoped.merge(options)`. `Relation#merge` now accepts a hash of + `Relation#merge` now accepts a hash of options, but they must be identical to the names of the equivalent finder method. These are mostly identical to the old-style finder option names, except in the following cases: @@ -67,7 +350,7 @@ * `:extend` becomes `:extending` The code to implement the deprecated features has been moved out to - the `active_record_deprecated_finders` gem. This gem is a dependency + the `activerecord-deprecated_finders` gem. This gem is a dependency of Active Record in Rails 4.0. It will no longer be a dependency from Rails 4.1, but if your app relies on the deprecated features then you can add it to your own Gemfile. It will be maintained by @@ -202,7 +485,7 @@ RAILS_ENV=production bundle exec rake db:schema:cache:dump => generate db/schema_cache.dump - 2) add config.use_schema_cache_dump = true in config/production.rb. BTW, true is default. + 2) add config.active_record.use_schema_cache_dump = true in config/production.rb. BTW, true is default. 3) boot rails. RAILS_ENV=production bundle exec rails server @@ -219,11 +502,11 @@ The `add_index` method now supports a `where` option that receives a string with the partial index criteria. - add_index(:accounts, :code, :where => "active") + add_index(:accounts, :code, :where => "active") - Generates + Generates - CREATE INDEX index_accounts_on_code ON accounts(code) WHERE active + CREATE INDEX index_accounts_on_code ON accounts(code) WHERE active *Marcelo Silveira* @@ -240,24 +523,13 @@ * Added the `ActiveRecord::NullRelation` class implementing the null object pattern for the Relation class. *Juanjo Bazán* -* Added deprecation for the `:dependent => :restrict` association option. - - Please note: - - * Up until now `has_many` and `has_one`, `:dependent => :restrict` - option raised a `DeleteRestrictionError` at the time of destroying - the object. Instead, it will add an error on the model. - - * To fix this warning, make sure your code isn't relying on a - `DeleteRestrictionError` and then add - `config.active_record.dependent_restrict_raises = false` to your - application config. +* Added new `:dependent => :restrict_with_error` option. This will add + an error to the model, rather than raising an exception. - * New rails application would be generated with the - `config.active_record.dependent_restrict_raises = false` in the - application config. + The `:restrict` option is renamed to `:restrict_with_exception` to + make this distinction explicit. - *Manoj Kumar* + *Manoj Kumar & Jon Leighton* * Added `create_join_table` migration helper to create HABTM join tables @@ -343,6 +615,79 @@ * PostgreSQL hstore types are automatically deserialized from the database. +## Rails 3.2.8 (Aug 9, 2012) ## + +* Do not consider the numeric attribute as changed if the old value is zero and the new value + is not a string. + Fixes #7237. + + *Rafael Mendonça França* + +* Removes the deprecation of `update_attribute`. *fxn* + +* Reverted the deprecation of `composed_of`. *Rafael Mendonça França* + +* Reverted the deprecation of `*_sql` association options. They will + be deprecated in 4.0 instead. *Jon Leighton* + +* Do not eager load AR session store. ActiveRecord::SessionStore depends on the abstract store + in Action Pack. Eager loading this class would break client code that eager loads Active Record + standalone. + Fixes #7160 + + *Xavier Noria* + +* Do not set RAILS_ENV to "development" when using `db:test:prepare` and related rake tasks. + This was causing the truncation of the development database data when using RSpec. + Fixes #7175. + + *Rafael Mendonça França* + + +## Rails 3.2.7 (Jul 26, 2012) ## + +* `:finder_sql` and `:counter_sql` options on collection associations + are deprecated. Please transition to using scopes. + + *Jon Leighton* + +* `:insert_sql` and `:delete_sql` options on `has_and_belongs_to_many` + associations are deprecated. Please transition to using `has_many + :through` + + *Jon Leighton* + +* `composed_of` has been deprecated. You'll have to write your own accessor + and mutator methods if you'd like to use value objects to represent some + portion of your models. + + *Steve Klabnik* + +* `update_attribute` has been deprecated. Use `update_column` if + you want to bypass mass-assignment protection, validations, callbacks, + and touching of updated_at. Otherwise please use `update_attributes`. + + *Steve Klabnik* + + +## Rails 3.2.6 (Jun 12, 2012) ## + +* protect against the nesting of hashes changing the + table context in the next call to build_from_hash. This fix + covers this case as well. + + CVE-2012-2695 + +* Revert earlier 'perf fix' (see 3.2.4 changelog / GH #6289). This + change introduced a regression (GH #6609). assoc.clear and + assoc.delete_all have loaded the association before doing the delete + since at least Rails 2.3. Doing the delete without loading the + records means that the `before_remove` and `after_remove` callbacks do + not get invoked. Therefore, this change was less a fix a more an + optimisation, which should only have gone into master. + + *Jon Leighton* + ## Rails 3.2.5 (Jun 1, 2012) ## @@ -700,7 +1045,7 @@ * LRU cache in mysql and sqlite are now per-process caches. - * lib/active_record/connection_adapters/mysql_adapter.rb: LRU cache keys are per process id. + * lib/active_record/connection_adapters/mysql_adapter.rb: LRU cache keys are per process id. * lib/active_record/connection_adapters/sqlite_adapter.rb: ditto *Aaron Patterson* @@ -1206,8 +1551,6 @@ ## Rails 3.0.0 (August 29, 2010) ## -* Changed update_attribute to not run callbacks and update the record directly in the database *Neeraj Singh* - * Add scoping and unscoped as the syntax to replace the old with_scope and with_exclusive_scope *José Valim* * New rake task, db:migrate:status, displays status of migrations #4947 *Kevin Skoglund* diff --git a/activerecord/RUNNING_UNIT_TESTS b/activerecord/RUNNING_UNIT_TESTS index 2c310e7ac3..bdd8834dcb 100644 --- a/activerecord/RUNNING_UNIT_TESTS +++ b/activerecord/RUNNING_UNIT_TESTS @@ -1,11 +1,10 @@ -== Configure databases +== Setup -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). +If you don't have the environment set make sure to read -You can build postgres and mysql databases using the postgresql:build_databases and mysql:build_databases rake tasks. + http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#testing-active-record. -== Running the tests +== Running the Tests You can run a particular test file from the command line, e.g. @@ -26,7 +25,7 @@ You can run all the tests for a given database via rake: The 'rake test' task will run all the tests for mysql, mysql2, sqlite3 and postgresql. -== Config file +== Custom 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 7feb0b75a0..a29d7b0e99 100644 --- a/activerecord/Rakefile +++ b/activerecord/Rakefile @@ -114,6 +114,16 @@ namespace :postgresql do config = ARTest.config['connections']['postgresql'] %x( createdb -E UTF8 #{config['arunit']['database']} ) %x( createdb -E UTF8 #{config['arunit2']['database']} ) + + # prepare hstore + version = %x( createdb --version ).strip.gsub(/(.*)(\d\.\d\.\d)$/, "\\2") + %w(arunit arunit2).each do |db| + if version < "9.1.0" + puts "Please prepare hstore data type. See http://www.postgresql.org/docs/9.0/static/hstore.html" + else + %x( psql #{config[db]['database']} -c "CREATE EXTENSION hstore;" ) + end + end end desc 'Drop the PostgreSQL test databases' diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec index dca7f13fd2..53791d96ef 100644 --- a/activerecord/activerecord.gemspec +++ b/activerecord/activerecord.gemspec @@ -24,5 +24,5 @@ Gem::Specification.new do |s| s.add_dependency('activemodel', version) s.add_dependency('arel', '~> 3.0.2') - s.add_dependency('active_record_deprecated_finders', '0.0.1') + s.add_dependency('activerecord-deprecated_finders', '0.0.1') end diff --git a/activerecord/examples/performance.rb b/activerecord/examples/performance.rb index 31f3e02bb8..cd9825b50c 100644 --- a/activerecord/examples/performance.rb +++ b/activerecord/examples/performance.rb @@ -1,7 +1,9 @@ -TIMES = (ENV['N'] || 10000).to_i - require File.expand_path('../../../load_paths', __FILE__) require "active_record" +require 'benchmark/ips' + +TIME = (ENV['BENCHMARK_TIME'] || 20).to_i +RECORDS = (ENV['BENCHMARK_RECORDS'] || TIME*1000).to_i conn = { :adapter => 'sqlite3', :database => ':memory:' } @@ -72,8 +74,8 @@ end notes = ActiveRecord::Faker::LOREM.join ' ' today = Date.today -puts 'Inserting 10,000 users and exhibits...' -10_000.times do +puts "Inserting #{RECORDS} users and exhibits..." +RECORDS.times do user = User.create( :created_at => today, :name => ActiveRecord::Faker.name, @@ -88,9 +90,7 @@ puts 'Inserting 10,000 users and exhibits...' ) end -require 'benchmark' - -Benchmark.bm(46) do |x| +Benchmark.ips(TIME) do |x| ar_obj = Exhibit.find(1) attrs = { :name => 'sam' } attrs_first = { :name => 'sam' } @@ -101,77 +101,72 @@ Benchmark.bm(46) do |x| :created_at => Date.today } - x.report("Model#id (x#{(TIMES * 100).ceil})") do - (TIMES * 100).ceil.times { ar_obj.id } + x.report("Model#id") do + ar_obj.id end x.report 'Model.new (instantiation)' do - TIMES.times { Exhibit.new } + Exhibit.new end x.report 'Model.new (setting attributes)' do - TIMES.times { Exhibit.new(attrs) } + Exhibit.new(attrs) end x.report 'Model.first' do - TIMES.times { Exhibit.first.look } + Exhibit.first.look end - x.report 'Model.named_scope' do - TIMES.times { Exhibit.limit(10).with_name.with_notes } + x.report("Model.all limit(100)") do + Exhibit.look Exhibit.limit(100) end - x.report("Model.all limit(100) (x#{(TIMES / 10).ceil})") do - (TIMES / 10).ceil.times { Exhibit.look Exhibit.limit(100) } + x.report "Model.all limit(100) with relationship" do + Exhibit.feel Exhibit.limit(100).includes(:user) end - x.report "Model.all limit(100) with relationship (x#{(TIMES / 10).ceil})" do - (TIMES / 10).ceil.times { Exhibit.feel Exhibit.limit(100).includes(:user) } + x.report "Model.all limit(10,000)" do + Exhibit.look Exhibit.limit(10000) end - x.report "Model.all limit(10,000) x(#{(TIMES / 1000).ceil})" do - (TIMES / 1000).ceil.times { Exhibit.look Exhibit.limit(10000) } + x.report 'Model.named_scope' do + Exhibit.limit(10).with_name.with_notes end x.report 'Model.create' do - TIMES.times { Exhibit.create(exhibit) } + Exhibit.create(exhibit) end x.report 'Resource#attributes=' do - TIMES.times { - exhibit = Exhibit.new(attrs_first) - exhibit.attributes = attrs_second - } + e = Exhibit.new(attrs_first) + e.attributes = attrs_second end x.report 'Resource#update' do - TIMES.times { Exhibit.first.update_attributes(:name => 'bob') } + Exhibit.first.update_attributes(:name => 'bob') end x.report 'Resource#destroy' do - TIMES.times { Exhibit.first.destroy } + Exhibit.first.destroy end x.report 'Model.transaction' do - TIMES.times { Exhibit.transaction { Exhibit.new } } + Exhibit.transaction { Exhibit.new } end x.report 'Model.find(id)' do - id = Exhibit.first.id - TIMES.times { Exhibit.find(id) } + User.find(1) end x.report 'Model.find_by_sql' do - TIMES.times { - Exhibit.find_by_sql("SELECT * FROM exhibits WHERE id = #{(rand * 1000 + 1).to_i}").first - } + Exhibit.find_by_sql("SELECT * FROM exhibits WHERE id = #{(rand * 1000 + 1).to_i}").first end - x.report "Model.log x(#{TIMES * 10})" do - (TIMES * 10).times { Exhibit.connection.send(:log, "hello", "world") {} } + x.report "Model.log" do + Exhibit.connection.send(:log, "hello", "world") {} end - x.report "AR.execute(query) (#{TIMES / 2})" do - (TIMES / 2).times { ActiveRecord::Base.connection.execute("Select * from exhibits where id = #{(rand * 1000 + 1).to_i}") } + x.report "AR.execute(query)" do + ActiveRecord::Base.connection.execute("Select * from exhibits where id = #{(rand * 1000 + 1).to_i}") end end diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index f8526bb691..fa94f6a941 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -22,15 +22,59 @@ #++ require 'active_support' +require 'active_support/rails' require 'active_model' require 'arel' -require 'active_record_deprecated_finders' +require 'active_record/deprecated_finders' require 'active_record/version' module ActiveRecord extend ActiveSupport::Autoload + autoload :Base + autoload :Callbacks + autoload :Core + autoload :CounterCache + autoload :ConnectionHandling + autoload :DynamicMatchers + autoload :Explain + autoload :Inheritance + autoload :Integration + autoload :Migration + autoload :Migrator, 'active_record/migration' + autoload :Model + autoload :ModelSchema + autoload :NestedAttributes + autoload :Observer + autoload :Persistence + autoload :QueryCache + autoload :Querying + autoload :ReadonlyAttributes + autoload :Reflection + autoload :Sanitization + + # ActiveRecord::SessionStore depends on the abstract store in Action Pack. + # Eager loading this class would break client code that eager loads Active + # Record standalone. + # + # Note that the Rails application generator creates an initializer specific + # for setting the session store. Thus, albeit in theory this autoload would + # not be thread-safe, in practice it is because if the application uses this + # session store its autoload happens at boot time. + autoload :SessionStore + + autoload :Schema + autoload :SchemaDumper + autoload :SchemaMigration + autoload :Scoping + autoload :Serialization + autoload :Store + autoload :Timestamp + autoload :Transactions + autoload :Translation + autoload :Validations + eager_autoload do autoload :ActiveRecordError, 'active_record/errors' autoload :ConnectionNotEstablished, 'active_record/errors' @@ -52,43 +96,10 @@ module ActiveRecord autoload :PredicateBuilder autoload :SpawnMethods autoload :Batches - autoload :Explain autoload :Delegation end - autoload :Base - autoload :Callbacks - autoload :Core - autoload :CounterCache - autoload :ConnectionHandling - autoload :DynamicMatchers - autoload :Explain - autoload :Inheritance - autoload :Integration - autoload :Migration - autoload :Migrator, 'active_record/migration' - autoload :Model - autoload :ModelSchema - autoload :NestedAttributes - autoload :Observer - autoload :Persistence - autoload :QueryCache - autoload :Querying - autoload :ReadonlyAttributes - autoload :Reflection autoload :Result - autoload :Sanitization - autoload :Schema - autoload :SchemaDumper - autoload :SchemaMigration - autoload :Scoping - autoload :Serialization - autoload :SessionStore - autoload :Store - autoload :Timestamp - autoload :Transactions - autoload :Translation - autoload :Validations end module Coders @@ -137,8 +148,27 @@ module ActiveRecord end end + module Tasks + extend ActiveSupport::Autoload + + autoload :DatabaseTasks + autoload :SQLiteDatabaseTasks, 'active_record/tasks/sqlite_database_tasks' + autoload :MySQLDatabaseTasks, 'active_record/tasks/mysql_database_tasks' + autoload :PostgreSQLDatabaseTasks, + 'active_record/tasks/postgresql_database_tasks' + end + autoload :TestCase autoload :TestFixtures, 'active_record/fixtures' + + def self.eager_load! + super + ActiveRecord::Locking.eager_load! + ActiveRecord::Scoping.eager_load! + ActiveRecord::Associations.eager_load! + ActiveRecord::AttributeMethods.eager_load! + ActiveRecord::ConnectionAdapters.eager_load! + end end ActiveSupport.on_load(:active_record) do diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb index 3ae7030caa..3db8e0716b 100644 --- a/activerecord/lib/active_record/aggregations.rb +++ b/activerecord/lib/active_record/aggregations.rb @@ -223,7 +223,7 @@ module ActiveRecord reader_method(name, class_name, mapping, allow_nil, constructor) writer_method(name, class_name, mapping, allow_nil, converter) - create_reflection(:composed_of, part_id, options, self) + create_reflection(:composed_of, part_id, nil, options, self) end private diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 68f8bbeb1c..9ba3323bc7 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1,9 +1,6 @@ require 'active_support/core_ext/enumerable' -require 'active_support/core_ext/module/delegation' -require 'active_support/core_ext/object/blank' require 'active_support/core_ext/string/conversions' require 'active_support/core_ext/module/remove_method' -require 'active_support/core_ext/class/attribute' module ActiveRecord class InverseOfAssociationNotFoundError < ActiveRecordError #:nodoc: @@ -104,6 +101,7 @@ module ActiveRecord # See ActiveRecord::Associations::ClassMethods for documentation. module Associations # :nodoc: + extend ActiveSupport::Autoload extend ActiveSupport::Concern # These classes will be loaded when associations are created. @@ -133,11 +131,13 @@ module ActiveRecord autoload :HasAndBelongsToMany, 'active_record/associations/builder/has_and_belongs_to_many' end - autoload :Preloader, 'active_record/associations/preloader' - autoload :JoinDependency, 'active_record/associations/join_dependency' - autoload :AssociationScope, 'active_record/associations/association_scope' - autoload :AliasTracker, 'active_record/associations/alias_tracker' - autoload :JoinHelper, 'active_record/associations/join_helper' + eager_autoload do + autoload :Preloader, 'active_record/associations/preloader' + autoload :JoinDependency, 'active_record/associations/join_dependency' + autoload :AssociationScope, 'active_record/associations/association_scope' + autoload :AliasTracker, 'active_record/associations/alias_tracker' + autoload :JoinHelper, 'active_record/associations/join_helper' + end # Clears out the association cache. def clear_association_cache #:nodoc: @@ -195,26 +195,6 @@ module ActiveRecord # * <tt>Project#categories.empty?, Project#categories.size, Project#categories, Project#categories<<(category1),</tt> # <tt>Project#categories.delete(category1)</tt> # - # === Overriding generated methods - # - # Association methods are generated in a module that is included into the model class, - # which allows you to easily override with your own methods and call the original - # generated method with +super+. For example: - # - # class Car < ActiveRecord::Base - # belongs_to :owner - # belongs_to :old_owner - # def owner=(new_owner) - # self.old_owner = self.owner - # super - # end - # end - # - # If your model class is <tt>Project</tt>, the module is - # named <tt>Project::GeneratedFeatureMethods</tt>. The GeneratedFeatureMethods module is - # included in the model class immediately after the (anonymous) generated attributes methods - # module, meaning an association will override the methods for an attribute with the same name. - # # === A word of warning # # Don't create associations that have the same name as instance methods of @@ -262,6 +242,26 @@ module ActiveRecord # others.uniq | X | X | X # others.reset | X | X | X # + # === Overriding generated methods + # + # Association methods are generated in a module that is included into the model class, + # which allows you to easily override with your own methods and call the original + # generated method with +super+. For example: + # + # class Car < ActiveRecord::Base + # belongs_to :owner + # belongs_to :old_owner + # def owner=(new_owner) + # self.old_owner = self.owner + # super + # end + # end + # + # If your model class is <tt>Project</tt>, the module is + # named <tt>Project::GeneratedFeatureMethods</tt>. The GeneratedFeatureMethods module is + # included in the model class immediately after the (anonymous) generated attributes methods + # module, meaning an association will override the methods for an attribute with the same name. + # # == Cardinality and associations # # Active Record associations can be used to describe one-to-one, one-to-many and many-to-many @@ -397,7 +397,28 @@ module ActiveRecord # * All unsaved (<tt>new_record? == true</tt>) members of the collection are automatically # saved when the parent is saved. # - # === Association callbacks + # == Customizing the query + # + # Associations are built from <tt>Relation</tt>s, and you can use the <tt>Relation</tt> syntax + # to customize them. For example, to add a condition: + # + # class Blog < ActiveRecord::Base + # has_many :published_posts, -> { where published: true }, class_name: 'Post' + # end + # + # Inside the <tt>-> { ... }</tt> block you can use all of the usual <tt>Relation</tt> methods. + # + # === Accessing the owner object + # + # Sometimes it is useful to have access to the owner object when building the query. The owner + # is passed as a parameter to the block. For example, the following association would find all + # events that occur on the user's birthday: + # + # class User < ActiveRecord::Base + # has_many :birthday_events, ->(user) { where starts_on: user.birthday }, class_name: 'Event' + # end + # + # == Association callbacks # # Similar to the normal callbacks that hook into the life cycle of an Active Record object, # you can also define callbacks that get triggered when you add an object to or remove an @@ -424,7 +445,7 @@ module ActiveRecord # added to the collection. Same with the +before_remove+ callbacks; if an exception is # thrown the object doesn't get removed. # - # === Association extensions + # == Association extensions # # The proxy objects that control the access to associations can be extended through anonymous # modules. This is especially beneficial for adding new finders, creators, and other @@ -454,20 +475,11 @@ module ActiveRecord # end # # class Account < ActiveRecord::Base - # has_many :people, :extend => FindOrCreateByNameExtension + # has_many :people, -> { extending FindOrCreateByNameExtension } # end # # class Company < ActiveRecord::Base - # has_many :people, :extend => FindOrCreateByNameExtension - # end - # - # If you need to use multiple named extension modules, you can specify an array of modules - # with the <tt>:extend</tt> option. - # In the case of name conflicts between methods in the modules, methods in modules later - # in the array supercede those earlier in the array. - # - # class Account < ActiveRecord::Base - # has_many :people, :extend => [FindOrCreateByNameExtension, FindRecentExtension] + # has_many :people, -> { extending FindOrCreateByNameExtension } # end # # Some extensions can only be made to work with knowledge of the association's internals. @@ -485,7 +497,7 @@ module ActiveRecord # the same object, allowing you to make calls like <tt>proxy_association.owner</tt> inside # association extensions. # - # === Association Join Models + # == 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 @@ -569,7 +581,7 @@ module ActiveRecord # belongs_to :tag, :inverse_of => :taggings # end # - # === Nested Associations + # == Nested Associations # # You can actually specify *any* association with the <tt>:through</tt> option, including an # association which has a <tt>:through</tt> option itself. For example: @@ -612,7 +624,7 @@ module ActiveRecord # add a <tt>Commenter</tt> in the example above, there would be no way to tell how to set up the # intermediate <tt>Post</tt> and <tt>Comment</tt> objects. # - # === Polymorphic Associations + # == 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 @@ -742,7 +754,7 @@ module ActiveRecord # to include an association which has conditions defined on it: # # class Post < ActiveRecord::Base - # has_many :approved_comments, :class_name => 'Comment', :conditions => ['approved = ?', true] + # has_many :approved_comments, -> { where approved: true }, :class_name => 'Comment' # end # # Post.includes(:approved_comments) @@ -754,14 +766,11 @@ module ActiveRecord # returning all the associated objects: # # class Picture < ActiveRecord::Base - # has_many :most_recent_comments, :class_name => 'Comment', :order => 'id DESC', :limit => 10 + # has_many :most_recent_comments, -> { order('id DESC').limit(10) }, :class_name => 'Comment' # end # # Picture.includes(:most_recent_comments).first.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. - # # Eager loading is supported with polymorphic associations. # # class Address < ActiveRecord::Base @@ -839,8 +848,8 @@ module ActiveRecord # module MyApplication # module Business # class Firm < ActiveRecord::Base - # has_many :clients - # end + # has_many :clients + # end # # class Client < ActiveRecord::Base; end # end @@ -938,7 +947,8 @@ module ActiveRecord # # The <tt>:dependent</tt> option can have different values which specify how the deletion # is done. For more information, see the documentation for this option on the different - # specific association types. + # specific association types. When no option is given, the behaviour is to do nothing + # with the associated records when destroying a record. # # === Delete or destroy? # @@ -1077,15 +1087,6 @@ module ActiveRecord # from the association name. So <tt>has_many :products</tt> will by default be linked # to the Product class, but if the real class name is SpecialProduct, you'll have to # 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 - # 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>. - # [:order] - # Specify the order in which the associated objects are returned as an <tt>ORDER BY</tt> SQL fragment, - # such as <tt>last_name, first_name DESC</tt>. # [: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_many+ @@ -1093,44 +1094,18 @@ module ActiveRecord # [:primary_key] # 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 - # objects' foreign keys are set to +NULL+ *without* calling their +save+ callbacks. If set to - # <tt>:restrict</tt> an error will be added to the object, preventing its deletion, if any associated - # objects are present. + # Controls what happens to the associated objects when + # their owner is destroyed: + # + # * <tt>:destroy</tt> causes all the associated objects to also be destroyed + # * <tt>:delete_all</tt> causes all the asssociated objects to be deleted directly from the database (so callbacks will not execute) + # * <tt>:nullify</tt> causes the foreign keys to be set to +NULL+. Callbacks are not executed. + # * <tt>:restrict_with_exception</tt> causes an exception to be raised if there are any associated records + # * <tt>:restrict_with_error</tt> causes an error to be added to the owner if there are 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 # the associated records. - # - # [:finder_sql] - # Specify a complete SQL statement to fetch the association. This is a good way to go for complex - # associations that depend on multiple tables. May be supplied as a string or a proc where interpolation is - # required. Note: When this option is used, +find_in_collection+ - # is _not_ added. - # [:counter_sql] - # Specify a complete SQL statement to fetch the size of the association. If <tt>:finder_sql</tt> is - # specified but not <tt>:counter_sql</tt>, <tt>:counter_sql</tt> will be generated by - # replacing <tt>SELECT ... FROM</tt> with <tt>SELECT COUNT(*) FROM</tt>. - # [:extend] - # Specify a named module for extending the proxy. See "Association extensions". - # [:include] - # Specify second-order associations that should be eager loaded when the collection is loaded. - # [:group] - # An attribute name by which the result should be grouped. Uses the <tt>GROUP BY</tt> SQL-clause. - # [:having] - # Combined with +:group+ this can be used to filter the records that a <tt>GROUP BY</tt> - # returns. Uses the <tt>HAVING</tt> SQL-clause. - # [:limit] - # An integer determining the limit on the number of rows that should be returned. - # [:offset] - # An integer determining the offset from where the rows should be fetched. So at 5, - # it would skip the first 4 rows. - # [:select] - # By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if - # you want to do a join but not include the joined columns, for example. Do not forget - # to include the primary and foreign keys, otherwise it will raise an error. # [:as] # Specifies a polymorphic interface (See <tt>belongs_to</tt>). # [:through] @@ -1157,16 +1132,14 @@ module ActiveRecord # [:source_type] # Specifies type of the source association used by <tt>has_many :through</tt> queries where the source # association is a polymorphic +belongs_to+. - # [:uniq] - # If true, duplicates will be omitted from the collection. Useful in conjunction with <tt>:through</tt>. - # [:readonly] - # If true, all the associated objects are readonly through the association. # [:validate] # If +false+, don't validate the associated objects when saving the parent object. true by default. # [:autosave] # If true, always save the associated objects or destroy them if marked for destruction, # when saving the parent object. If false, never save or destroy the associated objects. # By default, only save associated objects that are new records. + # + # Note that <tt>accepts_nested_attributes_for</tt> sets <tt>:autosave</tt> to <tt>true</tt>. # [:inverse_of] # Specifies the name of the <tt>belongs_to</tt> association on the associated object # that is the inverse of this <tt>has_many</tt> association. Does not work in combination @@ -1174,24 +1147,16 @@ module ActiveRecord # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail. # # Option examples: - # has_many :comments, :order => "posted_on" - # has_many :comments, :include => :author - # has_many :people, :class_name => "Person", :conditions => "deleted = 0", :order => "name" - # has_many :tracks, :order => "position", :dependent => :destroy - # has_many :comments, :dependent => :nullify - # has_many :tags, :as => :taggable - # has_many :reports, :readonly => true - # has_many :subscribers, :through => :subscriptions, :source => :user - # has_many :subscribers, :class_name => "Person", :finder_sql => Proc.new { - # %Q{ - # SELECT DISTINCT * - # FROM people p, post_subscriptions ps - # WHERE ps.post_id = #{id} AND ps.person_id = p.id - # ORDER BY p.first_name - # } - # } - def has_many(name, options = {}, &extension) - Builder::HasMany.build(self, name, options, &extension) + # has_many :comments, -> { order "posted_on" } + # has_many :comments, -> { includes :author } + # has_many :people, -> { where("deleted = 0").order("name") }, class_name: "Person" + # has_many :tracks, -> { order "position" }, dependent: :destroy + # has_many :comments, dependent: :nullify + # has_many :tags, as: :taggable + # has_many :reports, -> { readonly } + # has_many :subscribers, through: :subscriptions, source: :user + def has_many(name, scope = nil, options = {}, &extension) + Builder::HasMany.build(self, name, scope, options, &extension) end # Specifies a one-to-one association with another class. This method should only be used @@ -1239,34 +1204,23 @@ module ActiveRecord # Specify the class name of the association. Use it only if that name can't be inferred # from the association name. So <tt>has_one :manager</tt> will by default be linked to the Manager class, but # if the real class name is Person, you'll have to specify it with this option. - # [:conditions] - # Specify the conditions that the associated object must meet in order to be included as a +WHERE+ - # SQL fragment, such as <tt>rank = 5</tt>. Record creation from the association is scoped if a hash - # is used. <tt>has_one :account, :conditions => {:enabled => true}</tt> will create - # an enabled account with <tt>@company.create_account</tt> or <tt>@company.build_account</tt>. - # [:order] - # Specify the order in which the associated objects are returned as an <tt>ORDER BY</tt> SQL fragment, - # such as <tt>last_name, first_name DESC</tt>. # [:dependent] - # 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+. - # If set to <tt>:restrict</tt>, an error will be added to the object, preventing its deletion, if an - # associated object is present. + # Controls what happens to the associated object when + # its owner is destroyed: + # + # * <tt>:destroy</tt> causes the associated object to also be destroyed + # * <tt>:delete</tt> causes the asssociated object to be deleted directly from the database (so callbacks will not execute) + # * <tt>:nullify</tt> causes the foreign key to be set to +NULL+. Callbacks are not executed. + # * <tt>:restrict_with_exception</tt> causes an exception to be raised if there is an associated record + # * <tt>:restrict_with_error</tt> causes an error to be added to the owner if there is an 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 # will use "person_id" as the default <tt>:foreign_key</tt>. # [:primary_key] # Specify the method that returns the primary key used for the association. By default this is +id+. - # [:include] - # Specify second-order associations that should be eager loaded when this object is loaded. # [:as] # Specifies a polymorphic interface (See <tt>belongs_to</tt>). - # [:select] - # By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if - # you want to do a join but not include the joined columns, for example. 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>, # <tt>:primary_key</tt>, and <tt>:foreign_key</tt> are ignored, as the association uses the @@ -1280,14 +1234,14 @@ module ActiveRecord # [:source_type] # Specifies type of the source association used by <tt>has_one :through</tt> queries where the source # association is a polymorphic +belongs_to+. - # [:readonly] - # If true, the associated object is readonly through the association. # [:validate] # If +false+, don't validate the associated object when saving the parent object. +false+ by default. # [:autosave] # If true, always save the associated object or destroy it if marked for destruction, # when saving the parent object. If false, never save or destroy the associated object. # By default, only save the associated object if it's a new record. + # + # Note that <tt>accepts_nested_attributes_for</tt> sets <tt>:autosave</tt> to <tt>true</tt>. # [: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 @@ -1298,14 +1252,14 @@ module ActiveRecord # has_one :credit_card, :dependent => :destroy # destroys the associated credit card # has_one :credit_card, :dependent => :nullify # updates the associated records foreign # # key value to NULL rather than destroying it - # has_one :last_comment, :class_name => "Comment", :order => "posted_on" - # has_one :project_manager, :class_name => "Person", :conditions => "role = 'project_manager'" - # has_one :attachment, :as => :attachable - # has_one :boss, :readonly => :true - # has_one :club, :through => :membership - # has_one :primary_address, :through => :addressables, :conditions => ["addressable.primary = ?", true], :source => :addressable - def has_one(name, options = {}) - Builder::HasOne.build(self, name, options) + # has_one :last_comment, -> { order 'posted_on' }, :class_name => "Comment" + # has_one :project_manager, -> { where role: 'project_manager' }, :class_name => "Person" + # has_one :attachment, as: :attachable + # has_one :boss, readonly: :true + # has_one :club, through: :membership + # has_one :primary_address, -> { where primary: true }, through: :addressables, source: :addressable + def has_one(name, scope = nil, options = {}) + Builder::HasOne.build(self, name, scope, options) end # Specifies a one-to-one association with another class. This method should only be used @@ -1350,13 +1304,6 @@ module ActiveRecord # Specify the class name of the association. Use it only if that name can't be inferred # from the association name. So <tt>belongs_to :author</tt> will by default be linked to the Author class, but # if the real class name is Person, you'll have to specify it with this option. - # [: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>. - # [:select] - # By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed - # if you want to do a join but not include the joined columns, for example. Do not - # forget to include the primary and foreign keys, otherwise it will raise an error. # [:foreign_key] # Specify the foreign key used for the association. By default this is guessed to be the name # of the association with an "_id" suffix. So a class that defines a <tt>belongs_to :person</tt> @@ -1389,14 +1336,10 @@ module ActiveRecord # option (e.g., <tt>:counter_cache => :my_custom_counter</tt>.) # Note: Specifying a counter cache will add it to that model's list of readonly attributes # using +attr_readonly+. - # [:include] - # Specify second-order associations that should be eager loaded when this object is loaded. # [:polymorphic] # Specify this association is a polymorphic association by passing +true+. # Note: If you've enabled the counter cache, then you may want to add the counter cache attribute # to the +attr_readonly+ list in the associated classes (e.g. <tt>class Post; attr_readonly :comments_count; end</tt>). - # [:readonly] - # If true, the associated object is readonly through the association. # [:validate] # If +false+, don't validate the associated objects when saving the parent object. +false+ by default. # [:autosave] @@ -1404,6 +1347,8 @@ module ActiveRecord # saving the parent object. # If false, never save or destroy the associated object. # By default, only save the associated object if it's a new record. + # + # Note that <tt>accepts_nested_attributes_for</tt> sets <tt>:autosave</tt> to <tt>true</tt>. # [:touch] # If true, the associated object will be touched (the updated_at/on attributes set to now) # when this record is either saved or destroyed. If you specify a symbol, that attribute @@ -1415,18 +1360,18 @@ module ActiveRecord # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail. # # Option examples: - # belongs_to :firm, :foreign_key => "client_of" - # belongs_to :person, :primary_key => "name", :foreign_key => "person_name" - # belongs_to :author, :class_name => "Person", :foreign_key => "author_id" - # belongs_to :valid_coupon, :class_name => "Coupon", :foreign_key => "coupon_id", - # :conditions => 'discounts > #{payments_count}' - # belongs_to :attachable, :polymorphic => true - # belongs_to :project, :readonly => true - # belongs_to :post, :counter_cache => true - # belongs_to :company, :touch => true - # belongs_to :company, :touch => :employees_last_updated_at - def belongs_to(name, options = {}) - Builder::BelongsTo.build(self, name, options) + # belongs_to :firm, foreign_key: "client_of" + # belongs_to :person, primary_key: "name", foreign_key: "person_name" + # belongs_to :author, class_name: "Person", foreign_key: "author_id" + # belongs_to :valid_coupon, ->(o) { where "discounts > #{o.payments_count}" }, + # class_name: "Coupon", foreign_key: "coupon_id" + # belongs_to :attachable, polymorphic: true + # belongs_to :project, readonly: true + # belongs_to :post, counter_cache: true + # belongs_to :company, touch: true + # belongs_to :company, touch: :employees_last_updated_at + def belongs_to(name, scope = nil, options = {}) + Builder::BelongsTo.build(self, name, scope, options) end # Specifies a many-to-many relationship with another class. This associates two classes via an @@ -1538,47 +1483,6 @@ module ActiveRecord # By default this is guessed to be the name of the associated class in lower-case and "_id" suffixed. # So if a Person class makes a +has_and_belongs_to_many+ association to Project, # 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 - # 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>. - # [:order] - # Specify the order in which the associated objects are returned as an <tt>ORDER BY</tt> SQL fragment, - # such as <tt>last_name, first_name DESC</tt> - # [:uniq] - # If true, duplicate associated objects will be ignored by accessors and query methods. - # [:finder_sql] - # Overwrite the default generated SQL statement used to fetch the association with a manual statement - # [:counter_sql] - # Specify a complete SQL statement to fetch the size of the association. If <tt>:finder_sql</tt> is - # specified but not <tt>:counter_sql</tt>, <tt>:counter_sql</tt> will be generated by - # replacing <tt>SELECT ... FROM</tt> with <tt>SELECT COUNT(*) FROM</tt>. - # [:delete_sql] - # Overwrite the default generated SQL statement used to remove links between the associated - # classes with a manual statement. - # [:insert_sql] - # Overwrite the default generated SQL statement used to add links between the associated classes - # with a manual statement. - # [:extend] - # Anonymous module for extending the proxy, see "Association extensions". - # [:include] - # Specify second-order associations that should be eager loaded when the collection is loaded. - # [:group] - # An attribute name by which the result should be grouped. Uses the <tt>GROUP BY</tt> SQL-clause. - # [:having] - # Combined with +:group+ this can be used to filter the records that a <tt>GROUP BY</tt> returns. - # Uses the <tt>HAVING</tt> SQL-clause. - # [:limit] - # An integer determining the limit on the number of rows that should be returned. - # [:offset] - # An integer determining the offset from where the rows should be fetched. So at 5, - # it would skip the first 4 rows. - # [:select] - # By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if - # you want to do a join but exclude the joined columns, for example. Do not forget to include the primary - # and foreign keys, otherwise it will raise an error. # [:readonly] # If true, all the associated objects are readonly through the association. # [:validate] @@ -1589,16 +1493,16 @@ module ActiveRecord # If false, never save or destroy the associated objects. # By default, only save associated objects that are new records. # + # Note that <tt>accepts_nested_attributes_for</tt> sets <tt>:autosave</tt> to <tt>true</tt>. + # # Option examples: # has_and_belongs_to_many :projects - # has_and_belongs_to_many :projects, :include => [ :milestones, :manager ] - # has_and_belongs_to_many :nations, :class_name => "Country" - # has_and_belongs_to_many :categories, :join_table => "prods_cats" - # has_and_belongs_to_many :categories, :readonly => true - # has_and_belongs_to_many :active_projects, :join_table => 'developers_projects', :delete_sql => - # proc { |record| "DELETE FROM developers_projects WHERE active=1 AND developer_id = #{id} AND project_id = #{record.id}" } - def has_and_belongs_to_many(name, options = {}, &extension) - Builder::HasAndBelongsToMany.build(self, name, options, &extension) + # has_and_belongs_to_many :projects, -> { includes :milestones, :manager } + # has_and_belongs_to_many :nations, class_name: "Country" + # has_and_belongs_to_many :categories, join_table: "prods_cats" + # has_and_belongs_to_many :categories, -> { readonly } + def has_and_belongs_to_many(name, scope = nil, options = {}, &extension) + Builder::HasAndBelongsToMany.build(self, name, scope, options, &extension) end end end diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index e75003f261..9f47e7e631 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -1,5 +1,4 @@ require 'active_support/core_ext/array/wrap' -require 'active_support/core_ext/object/inclusion' module ActiveRecord module Associations @@ -81,10 +80,15 @@ module ActiveRecord loaded! end - def scoped + def scope target_scope.merge(association_scope) end + def scoped + ActiveSupport::Deprecation.warn("#scoped is deprecated. use #scope instead.") + scope + end + # The scope for this association. # # Note that the association_scope is merged into the target_scope only when the @@ -118,7 +122,7 @@ module ActiveRecord # Can be overridden (i.e. in ThroughAssociation) to merge in other scopes (i.e. the # through association's scope) def target_scope - klass.scoped + klass.all end # Loads the \target if needed and returns it. @@ -148,6 +152,21 @@ module ActiveRecord end end + # We can't dump @reflection since it contains the scope proc + def marshal_dump + reflection = @reflection + @reflection = nil + + ivars = instance_variables.map { |name| [name, instance_variable_get(name)] } + [reflection.name, ivars] + end + + def marshal_load(data) + reflection_name, ivars = data + ivars.each { |name, val| instance_variable_set(name, val) } + @reflection = @owner.class.reflect_on_association(reflection_name) + end + private def find_target? @@ -157,7 +176,7 @@ module ActiveRecord def creation_attributes attributes = {} - if reflection.macro.in?([:has_one, :has_many]) && !options[:through] + if (reflection.macro == :has_one || reflection.macro == :has_many) && !options[:through] attributes[reflection.foreign_key] = owner[reflection.active_record_primary_key] if reflection.options[:as] diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index 89a626693d..1303822868 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -6,7 +6,7 @@ module ActiveRecord attr_reader :association, :alias_tracker delegate :klass, :owner, :reflection, :interpolate, :to => :association - delegate :chain, :conditions, :options, :source_options, :active_record, :to => :reflection + delegate :chain, :scope_chain, :options, :source_options, :active_record, :to => :reflection def initialize(association) @association = association @@ -15,20 +15,7 @@ module ActiveRecord def scope scope = klass.unscoped - - scope.extending!(*Array(options[:extend])) - - # It's okay to just apply all these like this. The options will only be present if the - # association supports that option; this is enforced by the association builder. - scope.merge!(options.slice( - :readonly, :references, :order, :limit, :joins, :group, :having, :offset, :select, :uniq)) - - if options[:include] - scope.includes! options[:include] - elsif options[:through] - scope.includes! source_options[:include] - end - + scope.merge! eval_scope(klass, reflection.scope) if reflection.scope add_constraints(scope) end @@ -82,8 +69,6 @@ module ActiveRecord foreign_key = reflection.active_record_primary_key end - conditions = self.conditions[i] - if reflection == chain.last bind_val = bind scope, table.table_name, key.to_s, owner[foreign_key] scope = scope.where(table[key].eq(bind_val)) @@ -93,14 +78,6 @@ module ActiveRecord bind_val = bind scope, table.table_name, reflection.type.to_s, value scope = scope.where(table[reflection.type].eq(bind_val)) end - - conditions.each do |condition| - if options[:through] && condition.is_a?(Hash) - condition = disambiguate_condition(table, condition) - end - - scope = scope.where(interpolate(condition)) - end else constraint = table[key].eq(foreign_table[foreign_key]) @@ -110,13 +87,15 @@ module ActiveRecord end scope = scope.joins(join(foreign_table, constraint)) + end - conditions.each do |condition| - condition = interpolate(condition) - condition = disambiguate_condition(table, condition) unless i == 0 + # Exclude the scope of the association itself, because that + # was already merged in the #scope method. + (scope_chain[i] - [self.reflection.scope]).each do |scope_chain_item| + item = eval_scope(reflection.klass, scope_chain_item) - scope = scope.where(condition) - end + scope.includes! item.includes_values + scope.where_values += item.where_values end end @@ -138,19 +117,11 @@ module ActiveRecord end end - def disambiguate_condition(table, condition) - if condition.is_a?(Hash) - Hash[ - condition.map do |k, v| - if v.is_a?(Hash) - [k, v] - else - [table.table_alias || table.name, { k => v }] - end - end - ] + def eval_scope(klass, scope) + if scope.is_a?(Relation) + scope else - condition + klass.unscoped.instance_exec(owner, &scope) end end end diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index ddfc6f6c05..75f72c1a46 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -2,6 +2,11 @@ module ActiveRecord # = Active Record Belongs To Associations module Associations class BelongsToAssociation < SingularAssociation #:nodoc: + + def handle_dependency + target.send(options[:dependent]) if load_target + end + def replace(record) raise_on_type_mismatch(record) if record diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb index 9a6896dd55..1df876bf62 100644 --- a/activerecord/lib/active_record/associations/builder/association.rb +++ b/activerecord/lib/active_record/associations/builder/association.rb @@ -1,86 +1,106 @@ module ActiveRecord::Associations::Builder class Association #:nodoc: - class_attribute :valid_options - self.valid_options = [:class_name, :foreign_key, :select, :conditions, :include, :extend, :readonly, :validate, :references] + class << self + attr_accessor :valid_options + end - # Set by subclasses - class_attribute :macro + self.valid_options = [:class_name, :foreign_key, :validate] - attr_reader :model, :name, :options, :reflection + attr_reader :model, :name, :scope, :options, :reflection - def self.build(model, name, options) - new(model, name, options).build + def self.build(*args, &block) + new(*args, &block).build end - def initialize(model, name, options) - @model, @name, @options = model, name, options + def initialize(model, name, scope, options) + @model = model + @name = name + + if scope.is_a?(Hash) + @scope = nil + @options = scope + else + @scope = scope + @options = options + end + + if @scope && @scope.arity == 0 + prev_scope = @scope + @scope = proc { instance_exec(&prev_scope) } + end end def mixin @model.generated_feature_methods end + include Module.new { def build; end } + def build validate_options - reflection = model.create_reflection(self.class.macro, name, options, model) define_accessors - reflection + configure_dependency if options[:dependent] + @reflection = model.create_reflection(macro, name, scope, options, model) + super # provides an extension point + @reflection end - private + def macro + raise NotImplementedError + end - def validate_options - options.assert_valid_keys(self.class.valid_options) - end + def valid_options + Association.valid_options + end - def define_accessors - define_readers - define_writers - end + def validate_options + options.assert_valid_keys(valid_options) + end - def define_readers - name = self.name - mixin.redefine_method(name) do |*params| - association(name).reader(*params) + def define_accessors + define_readers + define_writers + end + + def define_readers + mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 + def #{name}(*args) + association(:#{name}).reader(*args) end - end + CODE + end - def define_writers - name = self.name - mixin.redefine_method("#{name}=") do |value| - association(name).writer(value) + def define_writers + mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 + def #{name}=(value) + association(:#{name}).writer(value) end - end + CODE + end - def dependent_restrict_raises? - ActiveRecord::Base.dependent_restrict_raises == true + def configure_dependency + unless valid_dependent_options.include? options[:dependent] + raise ArgumentError, "The :dependent option must be one of #{valid_dependent_options}, but is :#{options[:dependent]}" end - def dependent_restrict_deprecation_warning - if dependent_restrict_raises? - msg = "In the next release, `:dependent => :restrict` will not raise a `DeleteRestrictionError`. "\ - "Instead, it will add an error on the model. To fix this warning, make sure your code " \ - "isn't relying on a `DeleteRestrictionError` and then add " \ - "`config.active_record.dependent_restrict_raises = false` to your application config." - ActiveSupport::Deprecation.warn msg - end + if options[:dependent] == :restrict + ActiveSupport::Deprecation.warn( + "The :restrict option is deprecated. Please use :restrict_with_exception instead, which " \ + "provides the same functionality." + ) end - def define_restrict_dependency_method - name = self.name - mixin.redefine_method(dependency_method_name) do - has_one_macro = association(name).reflection.macro == :has_one - if has_one_macro ? !send(name).nil? : send(name).exists? - if dependent_restrict_raises? - raise ActiveRecord::DeleteRestrictionError.new(name) - else - key = has_one_macro ? "one" : "many" - errors.add(:base, :"restrict_dependent_destroy.#{key}", - :record => self.class.human_attribute_name(name).downcase) - return false - end - end + mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 + def #{macro}_dependent_for_#{name} + association(:#{name}).handle_dependency end - end + CODE + + model.before_destroy "#{macro}_dependent_for_#{name}" + end + + def valid_dependent_options + raise NotImplementedError + end end end diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb index 4183c222de..2f2600b7fb 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -1,10 +1,12 @@ -require 'active_support/core_ext/object/inclusion' - module ActiveRecord::Associations::Builder class BelongsTo < SingularAssociation #:nodoc: - self.macro = :belongs_to + def macro + :belongs_to + end - self.valid_options += [:foreign_type, :polymorphic, :touch] + def valid_options + super + [:foreign_type, :polymorphic, :touch] + end def constructable? !options[:polymorphic] @@ -14,74 +16,51 @@ module ActiveRecord::Associations::Builder reflection = super add_counter_cache_callbacks(reflection) if options[:counter_cache] add_touch_callbacks(reflection) if options[:touch] - configure_dependency reflection end - private + def add_counter_cache_callbacks(reflection) + cache_column = reflection.counter_cache_column - def add_counter_cache_callbacks(reflection) - cache_column = reflection.counter_cache_column - name = self.name - - method_name = "belongs_to_counter_cache_after_create_for_#{name}" - mixin.redefine_method(method_name) do - record = send(name) - record.class.increment_counter(cache_column, record.id) unless record.nil? + mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 + def belongs_to_counter_cache_after_create_for_#{name} + record = #{name} + record.class.increment_counter(:#{cache_column}, record.id) unless record.nil? end - model.after_create(method_name) - method_name = "belongs_to_counter_cache_before_destroy_for_#{name}" - mixin.redefine_method(method_name) do + def belongs_to_counter_cache_before_destroy_for_#{name} unless marked_for_destruction? - record = send(name) - record.class.decrement_counter(cache_column, record.id) unless record.nil? + record = #{name} + record.class.decrement_counter(:#{cache_column}, record.id) unless record.nil? end end - model.before_destroy(method_name) + CODE - model.send(:module_eval, - "#{reflection.class_name}.send(:attr_readonly,\"#{cache_column}\".intern) if defined?(#{reflection.class_name}) && #{reflection.class_name}.respond_to?(:attr_readonly)", __FILE__, __LINE__ - ) - end + model.after_create "belongs_to_counter_cache_after_create_for_#{name}" + model.before_destroy "belongs_to_counter_cache_before_destroy_for_#{name}" - def add_touch_callbacks(reflection) - name = self.name - method_name = "belongs_to_touch_after_save_or_destroy_for_#{name}" - touch = options[:touch] + klass = reflection.class_name.safe_constantize + klass.attr_readonly cache_column if klass && klass.respond_to?(:attr_readonly) + end - mixin.redefine_method(method_name) do - record = send(name) + def add_touch_callbacks(reflection) + mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 + def belongs_to_touch_after_save_or_destroy_for_#{name} + record = #{name} unless record.nil? - if touch == true - record.touch - else - record.touch(touch) - end + record.touch #{options[:touch].inspect if options[:touch] != true} end end + CODE - model.after_save(method_name) - model.after_touch(method_name) - model.after_destroy(method_name) - end - - def configure_dependency - if options[:dependent] - unless options[:dependent].in?([:destroy, :delete]) - raise ArgumentError, "The :dependent option expects either :destroy or :delete (#{options[:dependent].inspect})" - end + model.after_save "belongs_to_touch_after_save_or_destroy_for_#{name}" + model.after_touch "belongs_to_touch_after_save_or_destroy_for_#{name}" + model.after_destroy "belongs_to_touch_after_save_or_destroy_for_#{name}" + end - method_name = "belongs_to_dependent_#{options[:dependent]}_for_#{name}" - model.send(:class_eval, <<-eoruby, __FILE__, __LINE__ + 1) - def #{method_name} - association = #{name} - association.#{options[:dependent]} if association - end - eoruby - model.after_destroy method_name - end - end + def valid_dependent_options + [:destroy, :delete] + end end end diff --git a/activerecord/lib/active_record/associations/builder/collection_association.rb b/activerecord/lib/active_record/associations/builder/collection_association.rb index 768f70b6c9..1b382f7285 100644 --- a/activerecord/lib/active_record/associations/builder/collection_association.rb +++ b/activerecord/lib/active_record/associations/builder/collection_association.rb @@ -2,23 +2,19 @@ module ActiveRecord::Associations::Builder class CollectionAssociation < Association #:nodoc: CALLBACKS = [:before_add, :after_add, :before_remove, :after_remove] - self.valid_options += [ - :table_name, :order, :group, :having, :limit, :offset, :uniq, :finder_sql, - :counter_sql, :before_add, :after_add, :before_remove, :after_remove - ] - - attr_reader :block_extension - - def self.build(model, name, options, &extension) - new(model, name, options, &extension).build + def valid_options + super + [:table_name, :finder_sql, :counter_sql, :before_add, :after_add, :before_remove, :after_remove] end - def initialize(model, name, options, &extension) - super(model, name, options) + attr_reader :block_extension, :extension_module + + def initialize(*args, &extension) + super(*args) @block_extension = extension end def build + show_deprecation_warnings wrap_block_extension reflection = super CALLBACKS.each { |callback_name| define_callback(callback_name) } @@ -29,47 +25,61 @@ module ActiveRecord::Associations::Builder true end - private + def show_deprecation_warnings + [:finder_sql, :counter_sql].each do |name| + if options.include? name + ActiveSupport::Deprecation.warn("The :#{name} association option is deprecated. Please find an alternative (such as using scopes).") + end + end + end + + def wrap_block_extension + if block_extension + @extension_module = mod = Module.new(&block_extension) + silence_warnings do + model.parent.const_set(extension_module_name, mod) + end - def wrap_block_extension - options[:extend] = Array(options[:extend]) + prev_scope = @scope - if block_extension - silence_warnings do - model.parent.const_set(extension_module_name, Module.new(&block_extension)) - end - options[:extend].push("#{model.parent}::#{extension_module_name}".constantize) + if prev_scope + @scope = proc { |owner| instance_exec(owner, &prev_scope).extending(mod) } + else + @scope = proc { extending(mod) } end end + end - def extension_module_name - @extension_module_name ||= "#{model.to_s.demodulize}#{name.to_s.camelize}AssociationExtension" - end + def extension_module_name + @extension_module_name ||= "#{model.name.demodulize}#{name.to_s.camelize}AssociationExtension" + end - def define_callback(callback_name) - full_callback_name = "#{callback_name}_for_#{name}" + def define_callback(callback_name) + full_callback_name = "#{callback_name}_for_#{name}" - # TODO : why do i need method_defined? I think its because of the inheritance chain - model.class_attribute full_callback_name.to_sym unless model.method_defined?(full_callback_name) - model.send("#{full_callback_name}=", Array(options[callback_name.to_sym])) - end + # TODO : why do i need method_defined? I think its because of the inheritance chain + model.class_attribute full_callback_name.to_sym unless model.method_defined?(full_callback_name) + model.send("#{full_callback_name}=", Array(options[callback_name.to_sym])) + end - def define_readers - super + def define_readers + super - name = self.name - mixin.redefine_method("#{name.to_s.singularize}_ids") do - association(name).ids_reader + mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 + def #{name.to_s.singularize}_ids + association(:#{name}).ids_reader end - end + CODE + end - def define_writers - super + def define_writers + super - name = self.name - mixin.redefine_method("#{name.to_s.singularize}_ids=") do |ids| - association(name).ids_writer(ids) + mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 + def #{name.to_s.singularize}_ids=(ids) + association(:#{name}).ids_writer(ids) end - end + CODE + end end end 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 30fc44b4c2..bdac02b5bf 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 @@ -1,57 +1,39 @@ module ActiveRecord::Associations::Builder class HasAndBelongsToMany < CollectionAssociation #:nodoc: - self.macro = :has_and_belongs_to_many + def macro + :has_and_belongs_to_many + end - self.valid_options += [:join_table, :association_foreign_key, :delete_sql, :insert_sql] + def valid_options + super + [:join_table, :association_foreign_key, :delete_sql, :insert_sql] + end def build reflection = super - check_validity(reflection) define_destroy_hook reflection end - private + def show_deprecation_warnings + super - def define_destroy_hook - name = self.name - model.send(:include, Module.new { - class_eval <<-RUBY, __FILE__, __LINE__ + 1 - def destroy_associations - association(#{name.to_sym.inspect}).delete_all - super - end - RUBY - }) - end - - # TODO: These checks should probably be moved into the Reflection, and we should not be - # redefining the options[:join_table] value - instead we should define a - # reflection.join_table method. - def check_validity(reflection) - if reflection.association_foreign_key == reflection.foreign_key - raise ActiveRecord::HasAndBelongsToManyAssociationForeignKeyNeeded.new(reflection) + [:delete_sql, :insert_sql].each do |name| + if options.include? name + ActiveSupport::Deprecation.warn("The :#{name} association option is deprecated. Please find an alternative (such as using has_many :through).") end - - reflection.options[:join_table] ||= join_table_name( - model.send(:undecorated_table_name, model.to_s), - model.send(:undecorated_table_name, reflection.class_name) - ) end + end - # Generates a join table name from two provided table names. - # The names in the join table names end up in lexicographic order. - # - # join_table_name("members", "clubs") # => "clubs_members" - # join_table_name("members", "special_clubs") # => "members_special_clubs" - def join_table_name(first_table_name, second_table_name) - if first_table_name < second_table_name - join_table = "#{first_table_name}_#{second_table_name}" - else - join_table = "#{second_table_name}_#{first_table_name}" - end - - model.table_name_prefix + join_table + model.table_name_suffix - end + def define_destroy_hook + name = self.name + model.send(:include, Module.new { + class_eval <<-RUBY, __FILE__, __LINE__ + 1 + def destroy_associations + association(:#{name}).delete_all + super + end + RUBY + }) + end end end diff --git a/activerecord/lib/active_record/associations/builder/has_many.rb b/activerecord/lib/active_record/associations/builder/has_many.rb index d37d4e9d33..ab8225460a 100644 --- a/activerecord/lib/active_record/associations/builder/has_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_many.rb @@ -1,60 +1,15 @@ -require 'active_support/core_ext/object/inclusion' - module ActiveRecord::Associations::Builder class HasMany < CollectionAssociation #:nodoc: - self.macro = :has_many - - self.valid_options += [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of] - - def build - reflection = super - configure_dependency - reflection + def macro + :has_many end - private - - def configure_dependency - if options[:dependent] - unless options[:dependent].in?([:destroy, :delete_all, :nullify, :restrict]) - raise ArgumentError, "The :dependent option expects either :destroy, :delete_all, " \ - ":nullify or :restrict (#{options[:dependent].inspect})" - end - - dependent_restrict_deprecation_warning if options[:dependent] == :restrict - send("define_#{options[:dependent]}_dependency_method") - model.before_destroy dependency_method_name - end - end - - def define_destroy_dependency_method - name = self.name - mixin.redefine_method(dependency_method_name) do - send(name).each do |o| - # No point in executing the counter update since we're going to destroy the parent anyway - o.mark_for_destruction - end - - send(name).delete_all - end - end - - def define_delete_all_dependency_method - name = self.name - mixin.redefine_method(dependency_method_name) do - association(name).delete_all - end - end - - def define_nullify_dependency_method - name = self.name - mixin.redefine_method(dependency_method_name) do - send(name).delete_all - end - end + def valid_options + super + [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of] + end - def dependency_method_name - "has_many_dependent_for_#{name}" - end + def valid_dependent_options + [:destroy, :delete_all, :nullify, :restrict, :restrict_with_error, :restrict_with_exception] + end end end diff --git a/activerecord/lib/active_record/associations/builder/has_one.rb b/activerecord/lib/active_record/associations/builder/has_one.rb index bc8a212bee..0da564f402 100644 --- a/activerecord/lib/active_record/associations/builder/has_one.rb +++ b/activerecord/lib/active_record/associations/builder/has_one.rb @@ -1,56 +1,25 @@ -require 'active_support/core_ext/object/inclusion' - module ActiveRecord::Associations::Builder class HasOne < SingularAssociation #:nodoc: - self.macro = :has_one - - self.valid_options += [:order, :as] + def macro + :has_one + end - class_attribute :through_options - self.through_options = [:through, :source, :source_type] + def valid_options + valid = super + [:order, :as] + valid += [:through, :source, :source_type] if options[:through] + valid + end def constructable? !options[:through] end - def build - reflection = super - configure_dependency unless options[:through] - reflection + def configure_dependency + super unless options[:through] end - private - - def validate_options - valid_options = self.class.valid_options - valid_options += self.class.through_options if options[:through] - options.assert_valid_keys(valid_options) - end - - def configure_dependency - if options[:dependent] - unless options[:dependent].in?([:destroy, :delete, :nullify, :restrict]) - raise ArgumentError, "The :dependent option expects either :destroy, :delete, " \ - ":nullify or :restrict (#{options[:dependent].inspect})" - end - - dependent_restrict_deprecation_warning if options[:dependent] == :restrict - send("define_#{options[:dependent]}_dependency_method") - model.before_destroy dependency_method_name - end - end - - def define_destroy_dependency_method - name = self.name - mixin.redefine_method(dependency_method_name) do - association(name).delete - end - end - alias :define_delete_dependency_method :define_destroy_dependency_method - alias :define_nullify_dependency_method :define_destroy_dependency_method - - def dependency_method_name - "has_one_dependent_#{options[:dependent]}_for_#{name}" - end + def valid_dependent_options + [:destroy, :delete, :nullify, :restrict, :restrict_with_error, :restrict_with_exception] + end end end diff --git a/activerecord/lib/active_record/associations/builder/singular_association.rb b/activerecord/lib/active_record/associations/builder/singular_association.rb index 436b6c1524..6a5830e57f 100644 --- a/activerecord/lib/active_record/associations/builder/singular_association.rb +++ b/activerecord/lib/active_record/associations/builder/singular_association.rb @@ -1,6 +1,8 @@ module ActiveRecord::Associations::Builder class SingularAssociation < Association #:nodoc: - self.valid_options += [:remote, :dependent, :counter_cache, :primary_key, :inverse_of] + def valid_options + super + [:remote, :dependent, :counter_cache, :primary_key, :inverse_of] + end def constructable? true @@ -11,22 +13,20 @@ module ActiveRecord::Associations::Builder define_constructors if constructable? end - private - - def define_constructors - name = self.name - - mixin.redefine_method("build_#{name}") do |*params, &block| - association(name).build(*params, &block) + def define_constructors + mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 + def build_#{name}(*args, &block) + association(:#{name}).build(*args, &block) end - mixin.redefine_method("create_#{name}") do |*params, &block| - association(name).create(*params, &block) + def create_#{name}(*args, &block) + association(:#{name}).create(*args, &block) end - mixin.redefine_method("create_#{name}!") do |*params, &block| - association(name).create!(*params, &block) + def create_#{name}!(*args, &block) + association(:#{name}).create!(*args, &block) end - end + CODE + end end end diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index e94fe35170..b15df4f308 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -50,18 +50,7 @@ module ActiveRecord end else column = "#{reflection.quoted_table_name}.#{reflection.association_primary_key}" - relation = scoped - - including = (relation.eager_load_values + relation.includes_values).uniq - - if including.any? - join_dependency = ActiveRecord::Associations::JoinDependency.new(reflection.klass, including, []) - relation = join_dependency.join_associations.inject(relation) do |r, association| - association.join_relation(r) - end - end - - relation.pluck(column) + scope.pluck(column) end end @@ -82,7 +71,7 @@ module ActiveRecord if block_given? load_target.select.each { |e| yield e } else - scoped.select(select) + scope.select(select) end end @@ -93,7 +82,7 @@ module ActiveRecord if options[:finder_sql] find_by_scan(*args) else - scoped.find(*args) + scope.find(*args) end end end @@ -175,9 +164,9 @@ module ActiveRecord # Calculate sum using SQL, not Enumerable. def sum(*args) if block_given? - scoped.sum(*args) { |*block_args| yield(*block_args) } + scope.sum(*args) { |*block_args| yield(*block_args) } else - scoped.sum(*args) + scope.sum(*args) end end @@ -194,13 +183,13 @@ module ActiveRecord reflection.klass.count_by_sql(custom_counter_sql) else - if options[:uniq] + if association_scope.uniq_value # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL. column_name ||= reflection.klass.primary_key - count_options.merge!(:distinct => true) + count_options[:distinct] = true end - value = scoped.count(column_name, count_options) + value = scope.count(column_name, count_options) limit = options[:limit] offset = options[:offset] @@ -257,14 +246,14 @@ module ActiveRecord # +count_records+, which is a method descendants have to provide. def size if !find_target? || loaded? - if options[:uniq] + if association_scope.uniq_value target.uniq.size else target.size end - elsif !loaded? && options[:group] + elsif !loaded? && !association_scope.group_values.empty? load_target.size - elsif !loaded? && !options[:uniq] && target.is_a?(Array) + elsif !loaded? && !association_scope.uniq_value && target.is_a?(Array) unsaved_records = target.select { |r| r.new_record? } unsaved_records.size + count_records else @@ -281,12 +270,20 @@ module ActiveRecord load_target.size end - # Returns true if the collection is empty. Equivalent to - # <tt>collection.size.zero?</tt>. If the collection has not been already + # Returns true if the collection is empty. + # + # If the collection has been loaded or the <tt>:counter_sql</tt> option + # is provided, it is equivalent to <tt>collection.size.zero?</tt>. If the + # collection has not been loaded, it is equivalent to + # <tt>collection.exists?</tt>. If the collection has not already been # loaded and you are going to fetch the records anyway it is better to # check <tt>collection.length.zero?</tt>. def empty? - size.zero? + if loaded? || options[:counter_sql] + size.zero? + else + !scope.exists? + end end # Returns true if the collections is not empty. @@ -309,9 +306,9 @@ module ActiveRecord end end - def uniq(collection = load_target) + def uniq seen = {} - collection.find_all do |record| + load_target.find_all do |record| seen[record.id] = true unless seen.key?(record.id) end end @@ -335,7 +332,7 @@ module ActiveRecord include_in_memory?(record) else load_target if options[:finder_sql] - loaded? ? target.include?(record) : scoped.exists?(record) + loaded? ? target.include?(record) : scope.exists?(record) end else false @@ -355,7 +352,7 @@ module ActiveRecord callback(:before_add, record) yield(record) if block_given? - if options[:uniq] && index = @target.index(record) + if association_scope.uniq_value && index = @target.index(record) @target[index] = record else @target << record @@ -391,10 +388,9 @@ module ActiveRecord if options[:finder_sql] reflection.klass.find_by_sql(custom_finder_sql) else - scoped.all + scope.to_a end - records = options[:uniq] ? uniq(records) : records records.each { |record| set_inverse_instance(record) } records end @@ -452,7 +448,7 @@ module ActiveRecord end def create_scope - scoped.scope_for_create.stringify_keys + scope.scope_for_create.stringify_keys end def delete_or_destroy(records, method) @@ -577,8 +573,8 @@ module ActiveRecord def first_or_last(type, *args) args.shift if args.first.is_a?(Hash) && args.first.empty? - collection = fetch_first_or_last_using_find?(args) ? scoped : load_target - collection.send(type, *args) + collection = fetch_first_or_last_using_find?(args) ? scope : load_target + collection.send(type, *args).tap {|it| set_inverse_instance it } end end end diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index 2fb80fdc4c..ee8b816ef4 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -34,15 +34,25 @@ module ActiveRecord # is computed directly through SQL and does not trigger by itself the # instantiation of the actual post records. class CollectionProxy < Relation - delegate :target, :load_target, :loaded?, :to => :@association + def initialize(association) #:nodoc: + @association = association + super association.klass, association.klass.arel_table + merge! association.scope + end + + def target + @association.target + end + + def load_target + @association.load_target + end + + def loaded? + @association.loaded? + end ## - # :method: select - # - # :call-seq: - # select(select = nil) - # select(&block) - # # Works in two ways. # # *First:* Specify a subset of fields to be selected from the result set. @@ -96,13 +106,11 @@ module ActiveRecord # # #<Pet id: 2, name: "Spook">, # # #<Pet id: 3, name: "Choo-Choo"> # # ] + def select(select = nil, &block) + @association.select(select, &block) + end ## - # :method: find - # - # :call-seq: - # find(*args, &block) - # # Finds an object in the collection responding to the +id+. Uses the same # rules as +ActiveRecord::Base.find+. Returns +ActiveRecord::RecordNotFound++ # error if the object can not be found. @@ -129,13 +137,11 @@ module ActiveRecord # # #<Pet id: 2, name: "Spook", person_id: 1>, # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> # # ] + def find(*args, &block) + @association.find(*args, &block) + end ## - # :method: first - # - # :call-seq: - # first(limit = nil) - # # Returns the first record, or the first +n+ records, from the collection. # If the collection is empty, the first form returns +nil+, and the second # form returns an empty array. @@ -162,13 +168,11 @@ module ActiveRecord # another_person_without.pets # => [] # another_person_without.pets.first # => nil # another_person_without.pets.first(3) # => [] + def first(*args) + @association.first(*args) + end ## - # :method: last - # - # :call-seq: - # last(limit = nil) - # # Returns the last record, or the last +n+ records, from the collection. # If the collection is empty, the first form returns +nil+, and the second # form returns an empty array. @@ -195,13 +199,11 @@ module ActiveRecord # another_person_without.pets # => [] # another_person_without.pets.last # => nil # another_person_without.pets.last(3) # => [] + def last(*args) + @association.last(*args) + end ## - # :method: build - # - # :call-seq: - # build(attributes = {}, options = {}, &block) - # # Returns a new object of the collection type that has been instantiated # with +attributes+ and linked to this object, but have not yet been saved. # You can pass an array of attributes hashes, this will return an array @@ -226,13 +228,11 @@ module ActiveRecord # # person.pets.size # => 5 # size of the collection # person.pets.count # => 0 # count from database + def build(attributes = {}, options = {}, &block) + @association.build(attributes, options, &block) + end ## - # :method: create - # - # :call-seq: - # create(attributes = {}, options = {}, &block) - # # Returns a new object of the collection type that has been instantiated with # attributes, linked to this object and that has already been saved (if it # passes the validations). @@ -259,13 +259,11 @@ module ActiveRecord # # #<Pet id: 2, name: "Spook", person_id: 1>, # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> # # ] + def create(attributes = {}, options = {}, &block) + @association.create(attributes, options, &block) + end ## - # :method: create! - # - # :call-seq: - # create!(attributes = {}, options = {}, &block) - # # Like +create+, except that if the record is invalid, raises an exception. # # class Person @@ -279,13 +277,11 @@ module ActiveRecord # # person.pets.create!(name: nil) # # => ActiveRecord::RecordInvalid: Validation failed: Name can't be blank + def create!(attributes = {}, options = {}, &block) + @association.create!(attributes, options, &block) + end ## - # :method: concat - # - # :call-seq: - # concat(*records) - # # Add one or more records to the collection by setting their foreign keys # to the association's primary key. Since << flattens its argument list and # inserts each record, +push+ and +concat+ behave identically. Returns +self+ @@ -310,13 +306,11 @@ module ActiveRecord # # person.pets.concat([Pet.new(name: 'Brain'), Pet.new(name: 'Benny')]) # person.pets.size # => 5 + def concat(*records) + @association.concat(*records) + end ## - # :method: replace - # - # :call-seq: - # replace(other_array) - # # Replace this collection with +other_array+. This will perform a diff # and delete/add only records that have changed. # @@ -339,14 +333,12 @@ module ActiveRecord # # person.pets.replace(["doo", "ggie", "gaga"]) # # => ActiveRecord::AssociationTypeMismatch: Pet expected, got String + def replace(other_array) + @association.replace(other_array) + end ## - # :method: delete_all - # - # :call-seq: - # delete_all() - # - # Deletes all the records from the collection. For +has_many+ asssociations, + # Deletes all the records from the collection. For +has_many+ associations, # the deletion is done according to the strategy specified by the <tt>:dependent</tt> # option. Returns an array with the deleted records. # @@ -434,13 +426,11 @@ module ActiveRecord # # Pet.find(1, 2, 3) # # => ActiveRecord::RecordNotFound + def delete_all + @association.delete_all + end ## - # :method: destroy_all - # - # :call-seq: - # destroy_all() - # # Deletes the records of the collection directly from the database. # This will _always_ remove the records ignoring the +:dependent+ # option. @@ -463,15 +453,11 @@ module ActiveRecord # person.pets # => [] # # Pet.find(1) # => Couldn't find Pet with id=1 + def destroy_all + @association.destroy_all + end ## - # :method: delete - # - # :call-seq: - # delete(*records) - # delete(*fixnum_ids) - # delete(*string_ids) - # # Deletes the +records+ supplied and removes them from the collection. For # +has_many+ associations, the deletion is done according to the strategy # specified by the <tt>:dependent</tt> option. Returns an array with the @@ -586,13 +572,11 @@ module ActiveRecord # # #<Pet id: 2, name: "Spook", person_id: 1>, # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> # # ] + def delete(*records) + @association.delete(*records) + end ## - # :method: destroy - # - # :call-seq: - # destroy(*records) - # # Destroys the +records+ supplied and removes them from the collection. # This method will _always_ remove record from the database ignoring # the +:dependent+ option. Returns an array with the removed records. @@ -661,13 +645,11 @@ module ActiveRecord # person.pets # => [] # # Pet.find(4, 5, 6) # => ActiveRecord::RecordNotFound: Couldn't find all Pets with IDs (4, 5, 6) + def destroy(*records) + @association.destroy(*records) + end ## - # :method: uniq - # - # :call-seq: - # uniq() - # # Specifies whether the records should be unique or not. # # class Person < ActiveRecord::Base @@ -682,13 +664,11 @@ module ActiveRecord # # person.pets.select(:name).uniq # # => [#<Pet name: "Fancy-Fancy">] + def uniq + @association.uniq + end ## - # :method: count - # - # :call-seq: - # count() - # # Count all records using SQL. # # class Person < ActiveRecord::Base @@ -702,13 +682,11 @@ module ActiveRecord # # #<Pet id: 2, name: "Spook", person_id: 1>, # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> # # ] + def count(column_name = nil, options = {}) + @association.count(column_name, options) + end ## - # :method: size - # - # :call-seq: - # size() - # # Returns the size of the collection. If the collection hasn't been loaded, # it executes a <tt>SELECT COUNT(*)</tt> query. # @@ -729,13 +707,11 @@ module ActiveRecord # person.pets.size # => 3 # # Because the collection is already loaded, this will behave like # # collection.size and no SQL count query is executed. + def size + @association.size + end ## - # :method: length - # - # :call-seq: - # length() - # # Returns the size of the collection calling +size+ on the target. # If the collection has been already loaded, +length+ and +size+ are # equivalent. @@ -755,10 +731,11 @@ module ActiveRecord # # #<Pet id: 2, name: "Spook", person_id: 1>, # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> # # ] + def length + @association.length + end ## - # :method: empty? - # # Returns +true+ if the collection is empty. # # class Person < ActiveRecord::Base @@ -772,14 +749,11 @@ module ActiveRecord # # person.pets.count # => 0 # person.pets.empty? # => true + def empty? + @association.empty? + end ## - # :method: any? - # - # :call-seq: - # any? - # any?{|item| block} - # # Returns +true+ if the collection is not empty. # # class Person < ActiveRecord::Base @@ -809,14 +783,11 @@ module ActiveRecord # pet.group == 'dogs' # end # # => true + def any?(&block) + @association.any?(&block) + end ## - # :method: many? - # - # :call-seq: - # many? - # many?{|item| block} - # # Returns true if the collection has more than one record. # Equivalent to <tt>collection.size > 1</tt>. # @@ -851,13 +822,11 @@ module ActiveRecord # pet.group == 'cats' # end # # => true + def many?(&block) + @association.many?(&block) + end ## - # :method: include? - # - # :call-seq: - # include?(record) - # # Returns +true+ if the given object is present in the collection. # # class Person < ActiveRecord::Base @@ -868,17 +837,8 @@ module ActiveRecord # # person.pets.include?(Pet.find(20)) # => true # person.pets.include?(Pet.find(21)) # => false - delegate :select, :find, :first, :last, - :build, :create, :create!, - :concat, :replace, :delete_all, :destroy_all, :delete, :destroy, :uniq, - :sum, :count, :size, :length, :empty?, - :any?, :many?, :include?, - :to => :@association - - def initialize(association) #:nodoc: - @association = association - super association.klass, association.klass.arel_table - merge! association.scoped + def include?(record) + @association.include?(record) end alias_method :new, :build @@ -892,21 +852,21 @@ module ActiveRecord # method, which gets the current scope, which is this object, which # delegates to @association, and so on. def scoping - @association.scoped.scoping { yield } - end - - def spawn - scoped + @association.scope.scoping { yield } end - def scoped(options = nil) + # Returns a <tt>Relation</tt> object for the records in this association + def scope association = @association - super.extending! do + @association.scope.extending! do define_method(:proxy_association) { association } end end + # :nodoc: + alias spawn scope + # Equivalent to <tt>Array#==</tt>. Returns +true+ if the two arrays # contain the same number of elements and if each element is equal # to the corresponding element in the other array, otherwise returns 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 58d041ec1d..93618721bb 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 @@ -5,7 +5,7 @@ module ActiveRecord attr_reader :join_table def initialize(owner, reflection) - @join_table = Arel::Table.new(reflection.options[:join_table]) + @join_table = Arel::Table.new(reflection.join_table) super end diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index e631579087..74864d271f 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -7,6 +7,28 @@ module ActiveRecord # is provided by its child HasManyThroughAssociation. class HasManyAssociation < CollectionAssociation #:nodoc: + def handle_dependency + case options[:dependent] + when :restrict, :restrict_with_exception + raise ActiveRecord::DeleteRestrictionError.new(reflection.name) unless empty? + + when :restrict_with_error + unless empty? + record = klass.human_attribute_name(reflection.name).downcase + owner.errors.add(:base, :"restrict_dependent_destroy.many", record: record) + false + end + + else + if options[:dependent] == :destroy + # No point in executing the counter update since we're going to destroy the parent anyway + load_target.each(&:mark_for_destruction) + end + + delete_all + end + end + def insert_record(record, validate = true, raise = false) set_owner_attributes(record) @@ -38,7 +60,7 @@ module ActiveRecord elsif options[:counter_sql] || options[:finder_sql] reflection.klass.count_by_sql(custom_counter_sql) else - scoped.count + scope.count end # If there's nothing in the database and @target has no new records @@ -46,7 +68,7 @@ module ActiveRecord # documented side-effect of the method that may avoid an extra SELECT. @target ||= [] and loaded! if count == 0 - [options[:limit], count].compact.min + [association_scope.limit_value, count].compact.min end def has_cached_counter?(reflection = reflection) @@ -90,10 +112,10 @@ module ActiveRecord update_counter(-records.length) unless inverse_updates_counter_cache? else if records == :all - scope = scoped + scope = self.scope else keys = records.map { |r| r[reflection.association_primary_key] } - scope = scoped.where(reflection.association_primary_key => keys) + scope = self.scope.where(reflection.association_primary_key => keys) end if method == :delete_all 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 2683aaf5da..88ff11f953 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/object/blank' module ActiveRecord # = Active Record Has Many Through Association @@ -126,7 +125,7 @@ module ActiveRecord # even when we just want to delete everything. records = load_target if records == :all - scope = through_association.scoped + scope = through_association.scope scope.where! construct_join_attributes(*records) case method @@ -171,7 +170,7 @@ module ActiveRecord def find_target return [] unless target_reflection_has_associated_record? - scoped.all + scope.to_a end # NOTE - not sure that we can actually cope with inverses here diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 2131edbc20..dd7da59a86 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -1,26 +1,45 @@ -require 'active_support/core_ext/object/inclusion' module ActiveRecord # = Active Record Belongs To Has One Association module Associations class HasOneAssociation < SingularAssociation #:nodoc: - def replace(record, save = true) - raise_on_type_mismatch(record) if record - load_target - reflection.klass.transaction do - if target && target != record - remove_target!(options[:dependent]) unless target.destroyed? + def handle_dependency + case options[:dependent] + when :restrict, :restrict_with_exception + raise ActiveRecord::DeleteRestrictionError.new(reflection.name) if load_target + + when :restrict_with_error + if load_target + record = klass.human_attribute_name(reflection.name).downcase + owner.errors.add(:base, :"restrict_dependent_destroy.one", record: record) + false end - if record - set_owner_attributes(record) - set_inverse_instance(record) + else + delete + end + end + + def replace(record, save = true) + raise_on_type_mismatch(record) if record + load_target - if owner.persisted? && save && !record.save - nullify_owner_attributes(record) - set_owner_attributes(target) if target - raise RecordNotSaved, "Failed to save the new associated #{reflection.name}." + # If target and record are nil, or target is equal to record, + # we don't need to have transaction. + if (target || record) && target != record + reflection.klass.transaction do + remove_target!(options[:dependent]) if target && !target.destroyed? + + if record + set_owner_attributes(record) + set_inverse_instance(record) + + if owner.persisted? && save && !record.save + nullify_owner_attributes(record) + set_owner_attributes(target) if target + raise RecordNotSaved, "Failed to save the new associated #{reflection.name}." + end end end end @@ -36,7 +55,7 @@ module ActiveRecord when :destroy target.destroy when :nullify - target.update_attribute(reflection.foreign_key, nil) + target.update_columns(reflection.foreign_key => nil) end end end @@ -52,16 +71,19 @@ module ActiveRecord end def remove_target!(method) - if method.in?([:delete, :destroy]) - target.send(method) - else - nullify_owner_attributes(target) + case method + when :delete + target.delete + when :destroy + target.destroy + else + nullify_owner_attributes(target) - if target.persisted? && owner.persisted? && !target.save - set_owner_attributes(target) - raise RecordNotSaved, "Failed to remove the existing associated #{reflection.name}. " + - "The record failed to save when after its foreign key was set to nil." - end + if target.persisted? && owner.persisted? && !target.save + set_owner_attributes(target) + raise RecordNotSaved, "Failed to remove the existing associated #{reflection.name}. " + + "The record failed to save when after its foreign key was set to nil." + end end end diff --git a/activerecord/lib/active_record/associations/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/join_dependency/join_association.rb index 0d7d28e458..0d3b4dbab1 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -92,14 +92,21 @@ module ActiveRecord constraint = build_constraint(reflection, table, key, foreign_table, foreign_key) - conditions = self.conditions[i].dup - conditions << { reflection.type => foreign_klass.base_class.name } if reflection.type + scope_chain_items = scope_chain[i] - conditions.each do |condition| - condition = active_record.send(:sanitize_sql, interpolate(condition), table.table_alias || table.name) - condition = Arel.sql(condition) unless condition.is_a?(Arel::Node) + if reflection.type + scope_chain_items += [ + ActiveRecord::Relation.new(reflection.klass, table) + .where(reflection.type => foreign_klass.base_class.name) + ] + end + + scope_chain_items.each do |item| + unless item.is_a?(Relation) + item = ActiveRecord::Relation.new(reflection.klass, table).instance_exec(self, &item) + end - constraint = constraint.and(condition) + constraint = constraint.and(item.arel.constraints) unless item.arel.constraints.empty? end relation.from(join(table, constraint)) @@ -137,18 +144,8 @@ module ActiveRecord table.table_alias || table.name end - def conditions - @conditions ||= reflection.conditions.reverse - end - - private - - def interpolate(conditions) - if conditions.respond_to?(:to_proc) - instance_eval(&conditions) - else - conditions - end + def scope_chain + @scope_chain ||= reflection.scope_chain.reverse end end diff --git a/activerecord/lib/active_record/associations/join_helper.rb b/activerecord/lib/active_record/associations/join_helper.rb index cea6ad6944..5a41b40c8f 100644 --- a/activerecord/lib/active_record/associations/join_helper.rb +++ b/activerecord/lib/active_record/associations/join_helper.rb @@ -19,7 +19,7 @@ module ActiveRecord if reflection.source_macro == :has_and_belongs_to_many tables << alias_tracker.aliased_table_for( - (reflection.source_reflection || reflection).options[:join_table], + (reflection.source_reflection || reflection).join_table, table_alias_for(reflection, true) ) end diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb index 54705e4950..ce5bf15f10 100644 --- a/activerecord/lib/active_record/associations/preloader.rb +++ b/activerecord/lib/active_record/associations/preloader.rb @@ -42,7 +42,7 @@ module ActiveRecord autoload :HasAndBelongsToMany, 'active_record/associations/preloader/has_and_belongs_to_many' autoload :BelongsTo, 'active_record/associations/preloader/belongs_to' - attr_reader :records, :associations, :options, :model + attr_reader :records, :associations, :preload_scope, :model # Eager loads the named associations for the given Active Record record(s). # @@ -78,15 +78,10 @@ module ActiveRecord # [ :books, :author ] # { :author => :avatar } # [ :books, { :author => :avatar } ] - # - # +options+ contains options that will be passed to ActiveRecord::Base#find - # (which is called under the hood for preloading records). But it is passed - # only one level deep in the +associations+ argument, i.e. it's not passed - # to the child associations when +associations+ is a Hash. - def initialize(records, associations, options = {}) - @records = Array.wrap(records).compact.uniq - @associations = Array.wrap(associations) - @options = options + def initialize(records, associations, preload_scope = nil) + @records = Array.wrap(records).compact.uniq + @associations = Array.wrap(associations) + @preload_scope = preload_scope || Relation.new(nil, nil) end def run @@ -110,7 +105,7 @@ module ActiveRecord def preload_hash(association) association.each do |parent, child| - Preloader.new(records, parent, options).run + Preloader.new(records, parent, preload_scope).run Preloader.new(records.map { |record| record.send(parent) }.flatten, child).run end end @@ -125,7 +120,7 @@ module ActiveRecord def preload_one(association) grouped_records(association).each do |reflection, klasses| klasses.each do |klass, records| - preloader_for(reflection).new(klass, records, reflection, options).run + preloader_for(reflection).new(klass, records, reflection, preload_scope).run end end end diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index b4c3908b10..cbf5e734ea 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -2,16 +2,16 @@ module ActiveRecord module Associations class Preloader class Association #:nodoc: - attr_reader :owners, :reflection, :preload_options, :model, :klass - - def initialize(klass, owners, reflection, preload_options) - @klass = klass - @owners = owners - @reflection = reflection - @preload_options = preload_options || {} - @model = owners.first && owners.first.class - @scoped = nil - @owners_by_key = nil + attr_reader :owners, :reflection, :preload_scope, :model, :klass + + def initialize(klass, owners, reflection, preload_scope) + @klass = klass + @owners = owners + @reflection = reflection + @preload_scope = preload_scope + @model = owners.first && owners.first.class + @scope = nil + @owners_by_key = nil end def run @@ -24,12 +24,12 @@ module ActiveRecord raise NotImplementedError end - def scoped - @scoped ||= build_scope + def scope + @scope ||= build_scope end def records_for(ids) - scoped.where(association_key.in(ids)) + scope.where(association_key.in(ids)) end def table @@ -92,34 +92,29 @@ module ActiveRecord records_by_owner end + def reflection_scope + @reflection_scope ||= reflection.scope ? klass.unscoped.instance_exec(nil, &reflection.scope) : klass.unscoped + end + def build_scope scope = klass.unscoped scope.default_scoped = true - scope = scope.where(interpolate(options[:conditions])) - scope = scope.where(interpolate(preload_options[:conditions])) + values = reflection_scope.values + preload_values = preload_scope.values - scope = scope.select(preload_options[:select] || options[:select] || table[Arel.star]) - scope = scope.includes(preload_options[:include] || options[:include]) + scope.where_values = Array(values[:where]) + Array(preload_values[:where]) + scope.references_values = Array(values[:references]) + Array(preload_values[:references]) + + scope.select! preload_values[:select] || values[:select] || table[Arel.star] + scope.includes! preload_values[:includes] || values[:includes] if options[:as] - scope = scope.where( - klass.table_name => { - reflection.type => model.base_class.sti_name - } - ) + scope.where!(klass.table_name => { reflection.type => model.base_class.sti_name }) end scope end - - def interpolate(conditions) - if conditions.respond_to?(:to_proc) - klass.send(:instance_eval, &conditions) - else - conditions - end - end end end end diff --git a/activerecord/lib/active_record/associations/preloader/collection_association.rb b/activerecord/lib/active_record/associations/preloader/collection_association.rb index c248aeaaf6..e6cd35e7a1 100644 --- a/activerecord/lib/active_record/associations/preloader/collection_association.rb +++ b/activerecord/lib/active_record/associations/preloader/collection_association.rb @@ -6,7 +6,7 @@ module ActiveRecord private def build_scope - super.order(preload_options[:order] || options[:order]) + super.order(preload_scope.values[:order] || reflection_scope.values[:order]) end def preload diff --git a/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb index b77b667219..8e8925f0a9 100644 --- a/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb +++ b/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb @@ -6,7 +6,7 @@ module ActiveRecord def initialize(klass, records, reflection, preload_options) super - @join_table = Arel::Table.new(options[:join_table]).alias('t0') + @join_table = Arel::Table.new(reflection.join_table).alias('t0') end # Unlike the other associations, we want to get a raw array of rows so that we can diff --git a/activerecord/lib/active_record/associations/preloader/has_many_through.rb b/activerecord/lib/active_record/associations/preloader/has_many_through.rb index c6e9ede356..9a662d3f53 100644 --- a/activerecord/lib/active_record/associations/preloader/has_many_through.rb +++ b/activerecord/lib/active_record/associations/preloader/has_many_through.rb @@ -6,7 +6,7 @@ module ActiveRecord def associated_records_by_owner super.each do |owner, records| - records.uniq! if options[:uniq] + records.uniq! if reflection_scope.uniq_value end end end diff --git a/activerecord/lib/active_record/associations/preloader/has_one.rb b/activerecord/lib/active_record/associations/preloader/has_one.rb index 848448bb48..24728e9f01 100644 --- a/activerecord/lib/active_record/associations/preloader/has_one.rb +++ b/activerecord/lib/active_record/associations/preloader/has_one.rb @@ -14,7 +14,7 @@ module ActiveRecord private def build_scope - super.order(preload_options[:order] || options[:order]) + super.order(preload_scope.values[:order] || reflection_scope.values[:order]) end end diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb index ad6374d09a..1c1ba11c44 100644 --- a/activerecord/lib/active_record/associations/preloader/through_association.rb +++ b/activerecord/lib/active_record/associations/preloader/through_association.rb @@ -14,10 +14,7 @@ module ActiveRecord def associated_records_by_owner through_records = through_records_by_owner - ActiveRecord::Associations::Preloader.new( - through_records.values.flatten, - source_reflection.name, options - ).run + Preloader.new(through_records.values.flatten, source_reflection.name, reflection_scope).run through_records.each do |owner, records| records.map! { |r| r.send(source_reflection.name) }.flatten! @@ -28,10 +25,7 @@ module ActiveRecord private def through_records_by_owner - ActiveRecord::Associations::Preloader.new( - owners, through_reflection.name, - through_options - ).run + Preloader.new(owners, through_reflection.name, through_scope).run Hash[owners.map do |owner| through_records = Array.wrap(owner.send(through_reflection.name)) @@ -45,21 +39,22 @@ module ActiveRecord end] end - def through_options - through_options = {} + def through_scope + through_scope = through_reflection.klass.unscoped if options[:source_type] - through_options[:conditions] = { reflection.foreign_type => options[:source_type] } + through_scope.where! reflection.foreign_type => options[:source_type] else - if options[:conditions] - through_options[:include] = options[:include] || options[:source] - through_options[:conditions] = options[:conditions] + unless reflection_scope.where_values.empty? + through_scope.includes_values = reflection_scope.values[:includes] || options[:source] + through_scope.where_values = reflection_scope.values[:where] end - through_options[:order] = options[:order] + through_scope.order! reflection_scope.values[:order] + through_scope.references! reflection_scope.values[:references] end - through_options + through_scope end end end diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb index a1a921bcb4..b84cb4922d 100644 --- a/activerecord/lib/active_record/associations/singular_association.rb +++ b/activerecord/lib/active_record/associations/singular_association.rb @@ -35,11 +35,11 @@ module ActiveRecord private def create_scope - scoped.scope_for_create.stringify_keys.except(klass.primary_key) + scope.scope_for_create.stringify_keys.except(klass.primary_key) end def find_target - scoped.first.tap { |record| set_inverse_instance(record) } + scope.first.tap { |record| set_inverse_instance(record) } end # Implemented by subclasses diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index be890e5767..b9e014735b 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -15,7 +15,7 @@ module ActiveRecord scope = super chain[1..-1].each do |reflection| scope = scope.merge( - reflection.klass.scoped.with_default_scope. + reflection.klass.all.with_default_scope. except(:select, :create_with, :includes, :preload, :joins, :eager_load) ) end diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb index bf9fe70b31..d9989274c8 100644 --- a/activerecord/lib/active_record/attribute_assignment.rb +++ b/activerecord/lib/active_record/attribute_assignment.rb @@ -1,11 +1,24 @@ -require 'active_support/concern' module ActiveRecord + ActiveSupport.on_load(:active_record_config) do + mattr_accessor :whitelist_attributes, instance_accessor: false + mattr_accessor :mass_assignment_sanitizer, instance_accessor: false + end + module AttributeAssignment extend ActiveSupport::Concern include ActiveModel::MassAssignmentSecurity + included do + initialize_mass_assignment_sanitizer + end + module ClassMethods + def inherited(child) # :nodoc: + child.send :initialize_mass_assignment_sanitizer if self == Base + super + end + private # The primary key and inheritance column can never be set by mass-assignment for security reasons. @@ -14,6 +27,11 @@ module ActiveRecord default << 'id' unless primary_key.eql? 'id' default end + + def initialize_mass_assignment_sanitizer + attr_accessible(nil) if Model.whitelist_attributes + self.mass_assignment_sanitizer = Model.mass_assignment_sanitizer if Model.mass_assignment_sanitizer + end end # Allows you to set all the attributes at once by passing in a hash with keys @@ -64,12 +82,13 @@ module ActiveRecord # user.name # => "Josh" # user.is_admin? # => true def assign_attributes(new_attributes, options = {}) - return unless new_attributes + return if new_attributes.blank? - attributes = new_attributes.stringify_keys - multi_parameter_attributes = [] + attributes = new_attributes.stringify_keys + multi_parameter_attributes = [] nested_parameter_attributes = [] - @mass_assignment_options = options + previous_options = @mass_assignment_options + @mass_assignment_options = options unless options[:without_protection] attributes = sanitize_for_mass_assignment(attributes, mass_assignment_role) @@ -78,24 +97,17 @@ module ActiveRecord attributes.each do |k, v| if k.include?("(") multi_parameter_attributes << [ k, v ] - elsif respond_to?("#{k}=") - if v.is_a?(Hash) - nested_parameter_attributes << [ k, v ] - else - send("#{k}=", v) - end + elsif v.is_a?(Hash) + nested_parameter_attributes << [ k, v ] else - raise(UnknownAttributeError, "unknown attribute: #{k}") + _assign_attribute(k, v) end end - # assign any deferred nested attributes after the base attributes have been set - nested_parameter_attributes.each do |k,v| - send("#{k}=", v) - end - - @mass_assignment_options = nil - assign_multiparameter_attributes(multi_parameter_attributes) + assign_nested_parameter_attributes(nested_parameter_attributes) unless nested_parameter_attributes.empty? + assign_multiparameter_attributes(multi_parameter_attributes) unless multi_parameter_attributes.empty? + ensure + @mass_assignment_options = previous_options end protected @@ -110,6 +122,21 @@ module ActiveRecord private + def _assign_attribute(k, v) + public_send("#{k}=", v) + rescue NoMethodError + if respond_to?("#{k}=") + raise + else + raise UnknownAttributeError, "unknown attribute: #{k}" + end + end + + # Assign any deferred nested attributes after the base attributes have been set. + def assign_nested_parameter_attributes(pairs) + pairs.each { |k, v| _assign_attribute(k, v) } + end + # Instantiates objects for all attribute classes that needs more than one constructor parameter. This is done # by calling new on the column type or aggregation type (through composed_of) object with these parameters. # So having the pairs written_on(1) = "2004", written_on(2) = "6", written_on(3) = "24", will instantiate @@ -123,84 +150,27 @@ module ActiveRecord ) end - def instantiate_time_object(name, values) - if self.class.send(:create_time_zone_conversion_attribute?, name, column_for_attribute(name)) - Time.zone.local(*values) - else - Time.time_with_datetime_fallback(self.class.default_timezone, *values) - end - end - def execute_callstack_for_multiparameter_attributes(callstack) errors = [] callstack.each do |name, values_with_empty_parameters| begin - send(name + "=", read_value_from_parameter(name, values_with_empty_parameters)) + send("#{name}=", MultiparameterAttribute.new(self, name, values_with_empty_parameters).read_value) rescue => ex - errors << AttributeAssignmentError.new("error on assignment #{values_with_empty_parameters.values.inspect} to #{name}", ex, name) + errors << AttributeAssignmentError.new("error on assignment #{values_with_empty_parameters.values.inspect} to #{name} (#{ex.message})", ex, name) end end unless errors.empty? - raise MultiparameterAssignmentErrors.new(errors), "#{errors.size} error(s) on assignment of multiparameter attributes" - end - end - - def read_value_from_parameter(name, values_hash_from_param) - klass = (self.class.reflect_on_aggregation(name.to_sym) || column_for_attribute(name)).klass - if values_hash_from_param.values.all?{|v|v.nil?} - nil - elsif klass == Time - read_time_parameter_value(name, values_hash_from_param) - elsif klass == Date - read_date_parameter_value(name, values_hash_from_param) - else - read_other_parameter_value(klass, name, values_hash_from_param) - end - end - - def read_time_parameter_value(name, values_hash_from_param) - # If Date bits were not provided, error - raise "Missing Parameter" if [1,2,3].any?{|position| !values_hash_from_param.has_key?(position)} - max_position = extract_max_param_for_multiparameter_attributes(values_hash_from_param, 6) - # If Date bits were provided but blank, then return nil - return nil if (1..3).any? {|position| values_hash_from_param[position].blank?} - - set_values = (1..max_position).collect{|position| values_hash_from_param[position] } - # If Time bits are not there, then default to 0 - (3..5).each {|i| set_values[i] = set_values[i].blank? ? 0 : set_values[i]} - instantiate_time_object(name, set_values) - end - - def read_date_parameter_value(name, values_hash_from_param) - return nil if (1..3).any? {|position| values_hash_from_param[position].blank?} - set_values = [values_hash_from_param[1], values_hash_from_param[2], values_hash_from_param[3]] - begin - Date.new(*set_values) - 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 + error_descriptions = errors.map { |ex| ex.message }.join(",") + raise MultiparameterAssignmentErrors.new(errors), "#{errors.size} error(s) on assignment of multiparameter attributes [#{error_descriptions}]" end end - def read_other_parameter_value(klass, name, values_hash_from_param) - max_position = extract_max_param_for_multiparameter_attributes(values_hash_from_param) - values = (1..max_position).collect do |position| - raise "Missing Parameter" if !values_hash_from_param.has_key?(position) - values_hash_from_param[position] - end - klass.new(*values) - end - - def extract_max_param_for_multiparameter_attributes(values_hash_from_param, upper_cap = 100) - [values_hash_from_param.keys.max,upper_cap].min - end - def extract_callstack_for_multiparameter_attributes(pairs) attributes = { } - pairs.each do |pair| - multiparameter_name, value = pair + pairs.each do |(multiparameter_name, value)| attribute_name = multiparameter_name.split("(").first - attributes[attribute_name] = {} unless attributes.include?(attribute_name) + attributes[attribute_name] ||= {} parameter_value = value.empty? ? nil : type_cast_attribute_value(multiparameter_name, value) attributes[attribute_name][find_parameter_position(multiparameter_name)] ||= parameter_value @@ -217,5 +187,100 @@ module ActiveRecord multiparameter_name.scan(/\(([0-9]*).*\)/).first.first.to_i end + class MultiparameterAttribute #:nodoc: + attr_reader :object, :name, :values, :column + + def initialize(object, name, values) + @object = object + @name = name + @values = values + end + + def read_value + return if values.values.compact.empty? + + @column = object.class.reflect_on_aggregation(name.to_sym) || object.column_for_attribute(name) + klass = column.klass + + if klass == Time + read_time + elsif klass == Date + read_date + else + read_other(klass) + end + end + + private + + def instantiate_time_object(set_values) + if object.class.send(:create_time_zone_conversion_attribute?, name, column) + Time.zone.local(*set_values) + else + Time.time_with_datetime_fallback(object.class.default_timezone, *set_values) + end + end + + def read_time + # If column is a :time (and not :date or :timestamp) there is no need to validate if + # there are year/month/day fields + if column.type == :time + # if the column is a time set the values to their defaults as January 1, 1970, but only if they're nil + { 1 => 1970, 2 => 1, 3 => 1 }.each do |key,value| + values[key] ||= value + end + else + # else column is a timestamp, so if Date bits were not provided, error + validate_missing_parameters!([1,2,3]) + + # If Date bits were provided but blank, then return nil + return if blank_date_parameter? + end + + max_position = extract_max_param(6) + set_values = values.values_at(*(1..max_position)) + # If Time bits are not there, then default to 0 + (3..5).each { |i| set_values[i] = set_values[i].presence || 0 } + instantiate_time_object(set_values) + end + + def read_date + return if blank_date_parameter? + set_values = values.values_at(1,2,3) + begin + Date.new(*set_values) + rescue ArgumentError # if Date.new raises an exception on an invalid date + instantiate_time_object(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 + + def read_other(klass) + max_position = extract_max_param + positions = (1..max_position) + validate_missing_parameters!(positions) + + set_values = values.values_at(*positions) + klass.new(*set_values) + end + + # Checks whether some blank date parameter exists. Note that this is different + # than the validate_missing_parameters! method, since it just checks for blank + # positions instead of missing ones, and does not raise in case one blank position + # exists. The caller is responsible to handle the case of this returning true. + def blank_date_parameter? + (1..3).any? { |position| values[position].blank? } + end + + # If some position is not provided, it errors out a missing parameter exception. + def validate_missing_parameters!(positions) + if missing_parameter = positions.detect { |position| !values.key?(position) } + raise ArgumentError.new("Missing Parameter - #{name}(#{missing_parameter})") + end + end + + def extract_max_param(upper_cap = 100) + [values.keys.max, upper_cap].min + end + end end end diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 39ea885246..ced15bc330 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -1,5 +1,4 @@ require 'active_support/core_ext/enumerable' -require 'active_support/deprecation' module ActiveRecord # = Active Record Attribute Methods @@ -16,19 +15,6 @@ module ActiveRecord include TimeZoneConversion include Dirty include Serialization - - # 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)). - # (Alias for the protected read_attribute method). - def [](attr_name) - read_attribute(attr_name) - end - - # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+. - # (Alias for the protected write_attribute method). - def []=(attr_name, value) - write_attribute(attr_name, value) - end end module ClassMethods @@ -149,7 +135,9 @@ module ActiveRecord # Returns a hash of all the attributes with their names as keys and the values of the attributes as values. def attributes - Hash[@attributes.map { |name, _| [name, read_attribute(name)] }] + attribute_names.each_with_object({}) { |name, attrs| + attrs[name] = read_attribute(name) + } end # Returns an <tt>#inspect</tt>-like string for the value of the @@ -190,6 +178,19 @@ module ActiveRecord self.class.columns_hash[name.to_s] end + # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example, + # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)). + # (Alias for the protected read_attribute method). + def [](attr_name) + read_attribute(attr_name) + end + + # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+. + # (Alias for the protected write_attribute method). + def []=(attr_name, value) + write_attribute(attr_name, value) + end + protected def clone_attributes(reader_method = :read_attribute, attributes = {}) diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index 11c63591e3..60e5b0e2bb 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -1,12 +1,16 @@ -require 'active_support/core_ext/class/attribute' -require 'active_support/core_ext/object/blank' +require 'active_support/core_ext/module/attribute_accessors' module ActiveRecord + ActiveSupport.on_load(:active_record_config) do + mattr_accessor :partial_updates, instance_accessor: false + self.partial_updates = true + end + module AttributeMethods module Dirty extend ActiveSupport::Concern + include ActiveModel::Dirty - include AttributeMethods::Write included do if self < ::ActiveRecord::Timestamp @@ -14,7 +18,6 @@ module ActiveRecord end config_attribute :partial_updates - self.partial_updates = true end # Attempts to +save+ the record and clears changed attributes if successful. @@ -72,11 +75,8 @@ module ActiveRecord def _field_changed?(attr, old, value) if column = column_for_attribute(attr) - if column.number? && column.null && (old.nil? || old == 0) && value.blank? - # For nullable numeric columns, NULL gets stored in database for blank (i.e. '') values. - # Hence we don't record it as a change if the value changes from nil to ''. - # If an old value of 0 is set to '' we want this to get changed to nil as otherwise it'll - # be typecast back to 0 (''.to_i => 0) + if column.number? && (changes_from_nil_to_empty_string?(column, old, value) || + changes_from_zero_to_string?(old, value)) value = nil else value = column.type_cast(value) @@ -85,6 +85,19 @@ module ActiveRecord old != value end + + def changes_from_nil_to_empty_string?(column, old, value) + # For nullable numeric columns, NULL gets stored in database for blank (i.e. '') values. + # Hence we don't record it as a change if the value changes from nil to ''. + # If an old value of 0 is set to '' we want this to get changed to nil as otherwise it'll + # be typecast back to 0 (''.to_i => 0) + column.null && (old.nil? || old == 0) && value.blank? + end + + def changes_from_zero_to_string?(old, value) + # For columns with old 0 and value non-empty string + old == 0 && value.is_a?(String) && value.present? && value != '0' + end end end end diff --git a/activerecord/lib/active_record/attribute_methods/query.rb b/activerecord/lib/active_record/attribute_methods/query.rb index 1e841dc8e0..a8b23abb7c 100644 --- a/activerecord/lib/active_record/attribute_methods/query.rb +++ b/activerecord/lib/active_record/attribute_methods/query.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/object/blank' module ActiveRecord module AttributeMethods diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index dcc3d79de9..a7af086e43 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -1,13 +1,17 @@ module ActiveRecord + ActiveSupport.on_load(:active_record_config) do + mattr_accessor :attribute_types_cached_by_default, instance_accessor: false + end + module AttributeMethods module Read extend ActiveSupport::Concern ATTRIBUTE_TYPES_CACHED_BY_DEFAULT = [:datetime, :timestamp, :time, :date] + ActiveRecord::Model.attribute_types_cached_by_default = ATTRIBUTE_TYPES_CACHED_BY_DEFAULT included do - config_attribute :attribute_types_cached_by_default, :global => true - self.attribute_types_cached_by_default = ATTRIBUTE_TYPES_CACHED_BY_DEFAULT + config_attribute :attribute_types_cached_by_default end module ClassMethods diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb index 706fbf0546..bdda5bc009 100644 --- a/activerecord/lib/active_record/attribute_methods/serialization.rb +++ b/activerecord/lib/active_record/attribute_methods/serialization.rb @@ -6,10 +6,46 @@ module ActiveRecord included do # Returns a hash of all the attributes that have been specified for serialization as # keys and their class restriction as values. - config_attribute :serialized_attributes + class_attribute :serialized_attributes, instance_accessor: false self.serialized_attributes = {} end + module ClassMethods + # If you have an attribute that needs to be saved to the database as an object, and retrieved as the same object, + # then specify the name of that attribute using this method and it will be handled automatically. + # The serialization is done through YAML. If +class_name+ is specified, the serialized object must be of that + # class on retrieval or SerializationTypeMismatch will be raised. + # + # ==== Parameters + # + # * +attr_name+ - The field name that should be serialized. + # * +class_name+ - Optional, class name that the object type should be equal to. + # + # ==== Example + # # Serialize a preferences attribute + # class User < ActiveRecord::Base + # serialize :preferences + # end + def serialize(attr_name, class_name = Object) + include Behavior + + coder = if [:load, :dump].all? { |x| class_name.respond_to?(x) } + class_name + else + Coders::YAMLColumn.new(class_name) + end + + # merge new serialized attribute and create new hash to ensure that each class in inheritance hierarchy + # has its own hash of own serialized attributes + self.serialized_attributes = serialized_attributes.merge(attr_name.to_s => coder) + end + end + + def serialized_attributes + ActiveSupport::Deprecation.warn("Instance level serialized_attributes method is deprecated, please use class level method.") + defined?(@serialized_attributes) ? @serialized_attributes : self.class.serialized_attributes + end + class Type # :nodoc: def initialize(column) @column = column @@ -44,71 +80,50 @@ module ActiveRecord end end - module ClassMethods - # If you have an attribute that needs to be saved to the database as an object, and retrieved as the same object, - # then specify the name of that attribute using this method and it will be handled automatically. - # The serialization is done through YAML. If +class_name+ is specified, the serialized object must be of that - # class on retrieval or SerializationTypeMismatch will be raised. - # - # ==== Parameters - # - # * +attr_name+ - The field name that should be serialized. - # * +class_name+ - Optional, class name that the object type should be equal to. - # - # ==== Example - # # Serialize a preferences attribute - # class User < ActiveRecord::Base - # serialize :preferences - # end - def serialize(attr_name, class_name = Object) - coder = if [:load, :dump].all? { |x| class_name.respond_to?(x) } - class_name - else - Coders::YAMLColumn.new(class_name) - end + # This is only added to the model when serialize is called, which + # ensures we do not make things slower when serialization is not used. + module Behavior #:nodoc: + extend ActiveSupport::Concern - # merge new serialized attribute and create new hash to ensure that each class in inheritance hierarchy - # has its own hash of own serialized attributes - self.serialized_attributes = serialized_attributes.merge(attr_name.to_s => coder) - end - - def initialize_attributes(attributes, options = {}) #:nodoc: - serialized = (options.delete(:serialized) { true }) ? :serialized : :unserialized - super(attributes, options) + module ClassMethods + def initialize_attributes(attributes, options = {}) + serialized = (options.delete(:serialized) { true }) ? :serialized : :unserialized + super(attributes, options) - serialized_attributes.each do |key, coder| - if attributes.key?(key) - attributes[key] = Attribute.new(coder, attributes[key], serialized) + serialized_attributes.each do |key, coder| + if attributes.key?(key) + attributes[key] = Attribute.new(coder, attributes[key], serialized) + end end + + attributes end - attributes - end + private - private + def attribute_cast_code(attr_name) + if serialized_attributes.include?(attr_name) + "v.unserialized_value" + else + super + end + end + end - def attribute_cast_code(attr_name) - if serialized_attributes.include?(attr_name) - "v.unserialized_value" + def type_cast_attribute_for_write(column, value) + if column && coder = self.class.serialized_attributes[column.name] + Attribute.new(coder, value, :unserialized) else super end end - end - - def type_cast_attribute_for_write(column, value) - if column && coder = self.class.serialized_attributes[column.name] - Attribute.new(coder, value, :unserialized) - else - super - end - end - def read_attribute_before_type_cast(attr_name) - if serialized_attributes.include?(attr_name) - super.unserialized_value - else - super + def read_attribute_before_type_cast(attr_name) + if self.class.serialized_attributes.include?(attr_name) + super.unserialized_value + else + super + end end end end diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb index ac31b636db..d1e9d2de0e 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -1,7 +1,13 @@ -require 'active_support/core_ext/class/attribute' -require 'active_support/core_ext/object/inclusion' module ActiveRecord + ActiveSupport.on_load(:active_record_config) do + mattr_accessor :time_zone_aware_attributes, instance_accessor: false + self.time_zone_aware_attributes = false + + mattr_accessor :skip_time_zone_conversion_for_attributes, instance_accessor: false + self.skip_time_zone_conversion_for_attributes = [] + end + module AttributeMethods module TimeZoneConversion class Type # :nodoc: @@ -22,11 +28,8 @@ module ActiveRecord extend ActiveSupport::Concern included do - config_attribute :time_zone_aware_attributes, :global => true - self.time_zone_aware_attributes = false - + config_attribute :time_zone_aware_attributes, global: true config_attribute :skip_time_zone_conversion_for_attributes - self.skip_time_zone_conversion_for_attributes = [] end module ClassMethods @@ -56,10 +59,14 @@ module ActiveRecord unless time.acts_like?(:time) time = time.is_a?(String) ? Time.zone.parse(time) : time.to_time rescue time end - time = time.in_time_zone rescue nil if time - write_attribute(:#{attr_name}, original_time) - #{attr_name}_will_change! - @attributes_cache["#{attr_name}"] = time + zoned_time = time && time.in_time_zone rescue nil + rounded_time = round_usec(zoned_time) + rounded_value = round_usec(read_attribute("#{attr_name}")) + if (rounded_value != rounded_time) || (!rounded_value && original_time) + write_attribute("#{attr_name}", original_time) + #{attr_name}_will_change! + @attributes_cache["#{attr_name}"] = zoned_time + end end EOV generated_attribute_methods.module_eval(method_body, __FILE__, line) @@ -75,6 +82,12 @@ module ActiveRecord [:datetime, :timestamp].include?(column.type) end end + + private + def round_usec(value) + return unless value + value.change(:usec => 0) + end end end end diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index d545e7799d..290f57659d 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -127,23 +127,17 @@ module ActiveRecord module AutosaveAssociation extend ActiveSupport::Concern - ASSOCIATION_TYPES = %w{ HasOne HasMany BelongsTo HasAndBelongsToMany } - module AssociationBuilderExtension #:nodoc: - def self.included(base) - base.valid_options << :autosave - end - def build - reflection = super model.send(:add_autosave_association_callbacks, reflection) - reflection + super end end included do - ASSOCIATION_TYPES.each do |type| - Associations::Builder.const_get(type).send(:include, AssociationBuilderExtension) + Associations::Builder::Association.class_eval do + self.valid_options << :autosave + include AssociationBuilderExtension end end diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 189985b671..a4705b24ca 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -4,7 +4,6 @@ require 'active_support/benchmarkable' require 'active_support/dependencies' require 'active_support/descendants_tracker' require 'active_support/time' -require 'active_support/core_ext/class/attribute' require 'active_support/core_ext/class/attribute_accessors' require 'active_support/core_ext/class/delegating_attributes' require 'active_support/core_ext/array/extract_options' @@ -13,11 +12,8 @@ require 'active_support/core_ext/hash/indifferent_access' require 'active_support/core_ext/hash/slice' require 'active_support/core_ext/string/behavior' require 'active_support/core_ext/kernel/singleton_class' -require 'active_support/core_ext/module/delegation' require 'active_support/core_ext/module/introspection' require 'active_support/core_ext/object/duplicable' -require 'active_support/core_ext/object/blank' -require 'active_support/deprecation' require 'arel' require 'active_record/errors' require 'active_record/log_subscriber' @@ -329,4 +325,4 @@ module ActiveRecord #:nodoc: end end -ActiveSupport.run_load_hooks(:active_record, ActiveRecord::Model::DeprecationProxy) +ActiveSupport.run_load_hooks(:active_record, ActiveRecord::Model::DeprecationProxy.new) diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index a050fabf35..111208d0b9 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -34,7 +34,7 @@ module ActiveRecord # Examples: # class CreditCard < ActiveRecord::Base # # Strip everything but digits, so the user can specify "555 234 34" or - # # "5552-3434" or both will mean "55523434" + # # "5552-3434" and both will mean "55523434" # before_validation(:on => :create) do # self.number = number.gsub(/[^0-9]/, "") if attribute_present?("number") # end @@ -231,30 +231,6 @@ module ActiveRecord # Returns true or false depending on whether the proc is contained in the before_save callback chain on a Topic model. # module Callbacks - # We can't define callbacks directly on ActiveRecord::Model because - # it is a module. So we queue up the definitions and execute them - # when ActiveRecord::Model is included. - module Register #:nodoc: - def self.extended(base) - base.config_attribute :_callbacks_register - base._callbacks_register = [] - end - - def self.setup(base) - base._callbacks_register.each do |item| - base.send(*item) - end - end - - def define_callbacks(*args) - self._callbacks_register << [:define_callbacks, *args] - end - - def define_model_callbacks(*args) - self._callbacks_register << [:define_model_callbacks, *args] - end - end - extend ActiveSupport::Concern CALLBACKS = [ 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 c259e46073..347d794fa3 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -2,7 +2,6 @@ require 'thread' require 'monitor' require 'set' require 'active_support/core_ext/module/deprecation' -require 'timeout' module ActiveRecord # Raised when a connection could not be obtained within the connection @@ -70,6 +69,131 @@ module ActiveRecord # after which the Reaper will consider a connection reapable. (default # 5 seconds). class ConnectionPool + # Threadsafe, fair, FIFO queue. Meant to be used by ConnectionPool + # with which it shares a Monitor. But could be a generic Queue. + # + # The Queue in stdlib's 'thread' could replace this class except + # stdlib's doesn't support waiting with a timeout. + class Queue + def initialize(lock = Monitor.new) + @lock = lock + @cond = @lock.new_cond + @num_waiting = 0 + @queue = [] + end + + # Test if any threads are currently waiting on the queue. + def any_waiting? + synchronize do + @num_waiting > 0 + end + end + + # Return the number of threads currently waiting on this + # queue. + def num_waiting + synchronize do + @num_waiting + end + end + + # Add +element+ to the queue. Never blocks. + def add(element) + synchronize do + @queue.push element + @cond.signal + end + end + + # If +element+ is in the queue, remove and return it, or nil. + def delete(element) + synchronize do + @queue.delete(element) + end + end + + # Remove all elements from the queue. + def clear + synchronize do + @queue.clear + end + end + + # Remove the head of the queue. + # + # If +timeout+ is not given, remove and return the head the + # queue if the number of available elements is strictly + # greater than the number of threads currently waiting (that + # is, don't jump ahead in line). Otherwise, return nil. + # + # If +timeout+ is given, block if it there is no element + # available, waiting up to +timeout+ seconds for an element to + # become available. + # + # Raises: + # - ConnectionTimeoutError if +timeout+ is given and no element + # becomes available after +timeout+ seconds, + def poll(timeout = nil) + synchronize do + if timeout + no_wait_poll || wait_poll(timeout) + else + no_wait_poll + end + end + end + + private + + def synchronize(&block) + @lock.synchronize(&block) + end + + # Test if the queue currently contains any elements. + def any? + !@queue.empty? + end + + # A thread can remove an element from the queue without + # waiting if an only if the number of currently available + # connections is strictly greater than the number of waiting + # threads. + def can_remove_no_wait? + @queue.size > @num_waiting + end + + # Removes and returns the head of the queue if possible, or nil. + def remove + @queue.shift + end + + # Remove and return the head the queue if the number of + # available elements is strictly greater than the number of + # threads currently waiting. Otherwise, return nil. + def no_wait_poll + remove if can_remove_no_wait? + end + + # Waits on the queue up to +timeout+ seconds, then removes and + # returns the head of the queue. + def wait_poll(timeout) + @num_waiting += 1 + + t0 = Time.now + elapsed = 0 + loop do + @cond.wait(timeout - elapsed) + + return remove if any? + + elapsed = Time.now - t0 + raise ConnectionTimeoutError if elapsed >= timeout + end + ensure + @num_waiting -= 1 + end + end + # Every +frequency+ seconds, the reaper will call +reap+ on +pool+. # A reaper instantiated with a nil frequency will never reap the # connection pool. @@ -100,21 +224,6 @@ module ActiveRecord attr_accessor :automatic_reconnect, :checkout_timeout, :dead_connection_timeout attr_reader :spec, :connections, :size, :reaper - class Latch # :nodoc: - def initialize - @mutex = Mutex.new - @cond = ConditionVariable.new - end - - def release - @mutex.synchronize { @cond.broadcast } - end - - def await - @mutex.synchronize { @cond.wait @mutex } - end - end - # Creates a new ConnectionPool object. +spec+ is a ConnectionSpecification # object which describes database connection information (e.g. adapter, # host name, username, password, etc), as well as the maximum size for @@ -137,9 +246,18 @@ module ActiveRecord # default max pool size to 5 @size = (spec.config[:pool] && spec.config[:pool].to_i) || 5 - @latch = Latch.new @connections = [] @automatic_reconnect = true + + @available = Queue.new self + end + + # Hack for tests to be able to add connections. Do not call outside of tests + def insert_connection_for_test!(c) #:nodoc: + synchronize do + @connections << c + @available.add c + end end # Retrieve the connection associated with the current thread, or call @@ -197,6 +315,7 @@ module ActiveRecord conn.disconnect! end @connections = [] + @available.clear end end @@ -211,6 +330,10 @@ module ActiveRecord @connections.delete_if do |conn| conn.requires_reloading? end + @available.clear + @connections.each do |conn| + @available.add conn + end end end @@ -234,23 +357,10 @@ module ActiveRecord # Raises: # - PoolFullError: no connection can be obtained from the pool. def checkout - loop do - # Checkout an available connection - synchronize do - # Try to find a connection that hasn't been leased, and lease it - conn = connections.find { |c| c.lease } - - # If all connections were leased, and we have room to expand, - # create a new connection and lease it. - if !conn && connections.size < size - conn = checkout_new_connection - conn.lease - end - - return checkout_and_verify(conn) if conn - end - - Timeout.timeout(@checkout_timeout, PoolFullError) { @latch.await } + synchronize do + conn = acquire_connection + conn.lease + checkout_and_verify(conn) end end @@ -266,8 +376,9 @@ module ActiveRecord end release conn + + @available.add conn end - @latch.release end # Remove a connection from the connection pool. The connection will @@ -275,12 +386,14 @@ module ActiveRecord def remove(conn) synchronize do @connections.delete conn + @available.delete conn # FIXME: we might want to store the key on the connection so that removing # from the reserved hash will be a little easier. release conn + + @available.add checkout_new_connection if @available.any_waiting? end - @latch.release end # Removes dead connections from the pool. A dead connection can occur @@ -293,11 +406,35 @@ module ActiveRecord remove conn if conn.in_use? && stale > conn.last_use && !conn.active? end end - @latch.release end private + # Acquire a connection by one of 1) immediately removing one + # from the queue of available connections, 2) creating a new + # connection if the pool is not at capacity, 3) waiting on the + # queue for a connection to become available. + # + # Raises: + # - PoolFullError if a connection could not be acquired (FIXME: + # why not ConnectionTimeoutError? + def acquire_connection + if conn = @available.poll + conn + elsif @connections.size < @size + checkout_new_connection + else + t0 = Time.now + begin + @available.poll(@checkout_timeout) + rescue ConnectionTimeoutError + msg = 'could not obtain a database connection within %0.3f seconds (waited %0.3f seconds)' % + [@checkout_timeout, Time.now - t0] + raise PoolFullError, msg + end + end + end + def release(conn) thread_id = if @reserved_connections[current_connection_id] == conn current_connection_id @@ -311,11 +448,11 @@ module ActiveRecord end def new_connection - ActiveRecord::Base.send(spec.adapter_method, spec.config) + ActiveRecord::Model.send(spec.adapter_method, spec.config) end def current_connection_id #:nodoc: - ActiveRecord::Base.connection_id ||= Thread.current.object_id + ActiveRecord::Model.connection_id ||= Thread.current.object_id end def checkout_new_connection @@ -426,10 +563,12 @@ module ActiveRecord end def retrieve_connection_pool(klass) - pool = get_pool_for_class klass.name - return pool if pool - return nil if ActiveRecord::Model == klass - retrieve_connection_pool klass.active_record_super + if !(klass < Model::Tag) + get_pool_for_class('ActiveRecord::Model') # default connection + else + pool = get_pool_for_class(klass.name) + pool || retrieve_connection_pool(klass.superclass) + end end private 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 4c6d03a1d2..02459763f7 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -1,6 +1,12 @@ module ActiveRecord module ConnectionAdapters # :nodoc: module DatabaseStatements + def initialize + super + @_current_transaction_records = [] + @transaction_joinable = nil + end + # Converts an arel AST to SQL def to_sql(arel, binds = []) if arel.respond_to?(:ast) @@ -167,7 +173,7 @@ module ActiveRecord def transaction(options = {}) options.assert_valid_keys :requires_new, :joinable - last_transaction_joinable = defined?(@transaction_joinable) ? @transaction_joinable : nil + last_transaction_joinable = @transaction_joinable if options.has_key?(:joinable) @transaction_joinable = options[:joinable] else @@ -176,22 +182,19 @@ module ActiveRecord requires_new = options[:requires_new] || !last_transaction_joinable transaction_open = false - @_current_transaction_records ||= [] begin - if block_given? - if requires_new || open_transactions == 0 - if open_transactions == 0 - begin_db_transaction - elsif requires_new - create_savepoint - end - increment_open_transactions - transaction_open = true - @_current_transaction_records.push([]) + if requires_new || open_transactions == 0 + if open_transactions == 0 + begin_db_transaction + elsif requires_new + create_savepoint end - yield + increment_open_transactions + transaction_open = true + @_current_transaction_records.push([]) end + yield rescue Exception => database_transaction_rollback if transaction_open && !outside_transaction? transaction_open = false @@ -225,7 +228,7 @@ module ActiveRecord @_current_transaction_records.last.concat(save_point_records) end end - rescue Exception => database_transaction_rollback + rescue Exception if open_transactions == 0 rollback_db_transaction rollback_transaction_records(true) @@ -370,7 +373,7 @@ module ActiveRecord records.uniq.each do |record| begin record.rolledback!(rollback) - rescue Exception => e + rescue => e record.logger.error(e) if record.respond_to?(:logger) && record.logger end end @@ -385,7 +388,7 @@ module ActiveRecord records.uniq.each do |record| begin record.committed! - rescue Exception => e + rescue => e record.logger.error(e) if record.respond_to?(:logger) && record.logger end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb index 17377bad96..be6fda95b4 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb @@ -2,7 +2,7 @@ module ActiveRecord module ConnectionAdapters # :nodoc: module QueryCache class << self - def included(base) + def included(base) #:nodoc: dirties_query_cache base, :insert, :update, :delete end @@ -56,7 +56,7 @@ module ActiveRecord end def select_all(arel, name = nil, binds = []) - if @query_cache_enabled + if @query_cache_enabled && !locked?(arel) sql = to_sql(arel, binds) cache_sql(sql, binds) { super(sql, name, binds) } else @@ -65,6 +65,7 @@ module ActiveRecord end private + def cache_sql(sql, binds) result = if @query_cache[sql].key?(binds) @@ -83,6 +84,10 @@ module ActiveRecord result.collect { |row| row.dup } end end + + def locked?(arel) + arel.respond_to?(:locked) && arel.locked + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index 6f9f0399db..60a9eee7c7 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -31,7 +31,7 @@ module ActiveRecord # BigDecimals need to be put in a non-normalized form and quoted. when nil then "NULL" when BigDecimal then value.to_s('F') - when Numeric then value.to_s + when Numeric, ActiveSupport::Duration then value.to_s when Date, Time then "'#{quoted_date(value)}'" when Symbol then "'#{quote_string(value.to_s)}'" when Class then "'#{value.to_s}'" 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 df78ba6c5a..dca355aa93 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/object/blank' require 'date' require 'set' require 'bigdecimal' @@ -259,7 +258,7 @@ module ActiveRecord end # end EOV end - + # Adds index options to the indexes hash, keyed by column name # This is primarily used to track indexes that need to be created after the table # @@ -271,7 +270,7 @@ module ActiveRecord # Appends <tt>:datetime</tt> columns <tt>:created_at</tt> and # <tt>:updated_at</tt> to the table. def timestamps(*args) - options = { :null => false }.merge(args.extract_options!) + options = args.extract_options! column(:created_at, :datetime, options) column(:updated_at, :datetime, options) end @@ -282,7 +281,7 @@ module ActiveRecord index_options = options.delete(:index) args.each do |col| column("#{col}_id", :integer, options) - column("#{col}_type", :string, polymorphic.is_a?(Hash) ? polymorphic : options) unless polymorphic.nil? + column("#{col}_type", :string, polymorphic.is_a?(Hash) ? polymorphic : options) if polymorphic index(polymorphic ? %w(id type).map { |t| "#{col}_#{t}" } : "#{col}_id", index_options.is_a?(Hash) ? index_options : nil) if index_options end end @@ -441,17 +440,13 @@ module ActiveRecord # Adds a reference. Optionally adds a +type+ column, if <tt>:polymorphic</tt> option is provided. # <tt>references</tt> and <tt>belongs_to</tt> are acceptable. # - # t.references(:goat) - # t.references(:goat, :polymorphic => true) - # t.belongs_to(:goat) + # t.references(:user) + # t.belongs_to(:supplier, polymorphic: true) + # def references(*args) options = args.extract_options! - polymorphic = options.delete(:polymorphic) - index_options = options.delete(:index) - args.each do |col| - @base.add_column(@table_name, "#{col}_id", :integer, options) - @base.add_column(@table_name, "#{col}_type", :string, polymorphic.is_a?(Hash) ? polymorphic : options) unless polymorphic.nil? - @base.add_index(@table_name, polymorphic ? %w(id type).map { |t| "#{col}_#{t}" } : "#{col}_id", index_options.is_a?(Hash) ? index_options : nil) if index_options + args.each do |ref_name| + @base.add_reference(@table_name, ref_name, options) end end alias :belongs_to :references @@ -459,18 +454,16 @@ module ActiveRecord # Removes a reference. Optionally removes a +type+ column. # <tt>remove_references</tt> and <tt>remove_belongs_to</tt> are acceptable. # - # t.remove_references(:goat) - # t.remove_references(:goat, :polymorphic => true) - # t.remove_belongs_to(:goat) + # t.remove_references(:user) + # t.remove_belongs_to(:supplier, polymorphic: true) + # def remove_references(*args) options = args.extract_options! - polymorphic = options.delete(:polymorphic) - args.each do |col| - @base.remove_column(@table_name, "#{col}_id") - @base.remove_column(@table_name, "#{col}_type") unless polymorphic.nil? + args.each do |ref_name| + @base.remove_reference(@table_name, ref_name, options) end end - alias :remove_belongs_to :remove_references + alias :remove_belongs_to :remove_references # Adds a column or columns of a specified type # 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 5758ac4569..86d6266af9 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -1,4 +1,3 @@ -require 'active_support/deprecation/reporting' require 'active_record/migration/join_table' module ActiveRecord @@ -57,7 +56,6 @@ module ActiveRecord # Checks to see if a column exists in a given table. # - # === Examples # # Check a column exists # column_exists?(:suppliers, :name) # @@ -65,13 +63,18 @@ module ActiveRecord # column_exists?(:suppliers, :name, :string) # # # Check a column exists with a specific definition - # column_exists?(:suppliers, :name, :string, :limit => 100) + # column_exists?(:suppliers, :name, :string, limit: 100) + # column_exists?(:suppliers, :name, :string, default: 'default') + # column_exists?(:suppliers, :name, :string, null: false) + # column_exists?(:suppliers, :tax, :decimal, precision: 8, scale: 2) def column_exists?(table_name, column_name, type = nil, options = {}) columns(table_name).any?{ |c| c.name == column_name.to_s && - (!type || c.type == type) && - (!options[:limit] || c.limit == options[:limit]) && - (!options[:precision] || c.precision == options[:precision]) && - (!options[:scale] || c.scale == options[:scale]) } + (!type || c.type == type) && + (!options.key?(:limit) || c.limit == options[:limit]) && + (!options.key?(:precision) || c.precision == options[:precision]) && + (!options.key?(:scale) || c.scale == options[:scale]) && + (!options.key?(:default) || c.default == options[:default]) && + (!options.key?(:null) || c.null == options[:null]) } end # Creates a new table with the name +table_name+. +table_name+ may either @@ -200,11 +203,14 @@ module ActiveRecord join_table_name = find_join_table_name(table_1, table_2, options) column_options = options.delete(:column_options) || {} - column_options.reverse_merge!({:null => false}) + column_options.reverse_merge!(null: false) - create_table(join_table_name, options.merge!(:id => false)) do |td| - td.integer :"#{table_1.to_s.singularize}_id", column_options - td.integer :"#{table_2.to_s.singularize}_id", column_options + t1_column, t2_column = [table_1, table_2].map{ |t| t.to_s.singularize.foreign_key } + + create_table(join_table_name, options.merge!(id: false)) do |td| + td.integer t1_column, column_options + td.integer t2_column, column_options + yield td if block_given? end end @@ -439,6 +445,42 @@ module ActiveRecord indexes(table_name).detect { |i| i.name == index_name } end + # Adds a reference. Optionally adds a +type+ column, if <tt>:polymorphic</tt> option is provided. + # <tt>add_reference</tt> and <tt>add_belongs_to</tt> are acceptable. + # + # ====== Create a user_id column + # add_reference(:products, :user) + # + # ====== Create a supplier_id and supplier_type columns + # add_belongs_to(:products, :supplier, polymorphic: true) + # + # ====== Create a supplier_id, supplier_type columns and appropriate index + # add_reference(:products, :supplier, polymorphic: true, index: true) + # + def add_reference(table_name, ref_name, options = {}) + polymorphic = options.delete(:polymorphic) + index_options = options.delete(:index) + add_column(table_name, "#{ref_name}_id", :integer, options) + add_column(table_name, "#{ref_name}_type", :string, polymorphic.is_a?(Hash) ? polymorphic : options) if polymorphic + add_index(table_name, polymorphic ? %w[id type].map{ |t| "#{ref_name}_#{t}" } : "#{ref_name}_id", index_options.is_a?(Hash) ? index_options : nil) if index_options + end + alias :add_belongs_to :add_reference + + # Removes the reference(s). Also removes a +type+ column if one exists. + # <tt>remove_reference</tt>, <tt>remove_references</tt> and <tt>remove_belongs_to</tt> are acceptable. + # + # ====== Remove the reference + # remove_reference(:products, :user, index: true) + # + # ====== Remove polymorphic reference + # remove_reference(:products, :supplier, polymorphic: true) + # + def remove_reference(table_name, ref_name, options = {}) + remove_column(table_name, "#{ref_name}_id") + remove_column(table_name, "#{ref_name}_type") if options[:polymorphic] + end + alias :remove_belongs_to :remove_reference + # Returns a string of <tt>CREATE TABLE</tt> SQL statement(s) for recreating the # entire structure of the database. def structure_dump @@ -447,7 +489,7 @@ module ActiveRecord def dump_schema_information #:nodoc: sm_table = ActiveRecord::Migrator.schema_migrations_table_name - ActiveRecord::SchemaMigration.order('version').all.map { |sm| + ActiveRecord::SchemaMigration.order('version').map { |sm| "INSERT INTO #{sm_table} (version) VALUES ('#{sm.version}');" }.join "\n\n" end @@ -548,7 +590,7 @@ module ActiveRecord if options.is_a?(Hash) && order = options[:order] case order when Hash - column_names.each {|name| option_strings[name] += " #{order[name].to_s.upcase}" if order.has_key?(name)} + column_names.each {|name| option_strings[name] += " #{order[name].upcase}" if order.has_key?(name)} when String column_names.each {|name| option_strings[name] += " #{order.upcase}"} end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index c6faae77cc..b3f9187429 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -2,7 +2,6 @@ require 'date' require 'bigdecimal' require 'bigdecimal/util' require 'active_support/core_ext/benchmark' -require 'active_support/deprecation' require 'active_record/connection_adapters/schema_cache' require 'monitor' @@ -286,7 +285,7 @@ module ActiveRecord :name => name, :connection_id => object_id, :binds => binds) { yield } - rescue Exception => e + rescue => e message = "#{e.class.name}: #{e.message}: #{sql}" @logger.error message if @logger exception = translate_exception(e, message) diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index 692473abc5..1126fe7fce 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/object/blank' require 'arel/visitors/bind_visitor' module ActiveRecord @@ -72,6 +71,8 @@ module ActiveRecord when /^mediumint/i; 3 when /^smallint/i; 2 when /^tinyint/i; 1 + when /^enum\((.+)\)/i + $1.split(',').map{|enum| enum.strip.length - 2}.max else super end @@ -264,19 +265,19 @@ module ActiveRecord def begin_db_transaction execute "BEGIN" - rescue Exception + rescue # Transactions aren't supported end def commit_db_transaction #:nodoc: execute "COMMIT" - rescue Exception + rescue # Transactions aren't supported end def rollback_db_transaction #:nodoc: execute "ROLLBACK" - rescue Exception + rescue # Transactions aren't supported end @@ -316,7 +317,7 @@ module ActiveRecord select_all(sql, 'SCHEMA').map { |table| table.delete('Table_type') sql = "SHOW CREATE TABLE #{quote_table_name(table.to_a.first.last)}" - exec_without_stmt(sql, 'SCHEMA').first['Create Table'] + ";\n\n" + exec_query(sql, 'SCHEMA').first['Create Table'] + ";\n\n" }.join end diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index 01bd3ae26c..1445bb3b2f 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -1,5 +1,4 @@ require 'set' -require 'active_support/deprecation' module ActiveRecord # :stopdoc: @@ -209,7 +208,7 @@ module ActiveRecord # '0.123456' -> 123456 # '1.123456' -> 123456 def microseconds(time) - ((time[:sec_fraction].to_f % 1) * 1_000_000).to_i + time[:sec_fraction] ? (time[:sec_fraction] * 1_000_000).to_i : 0 end def new_date(year, mon, mday) @@ -234,7 +233,7 @@ module ActiveRecord # Doesn't handle time zones. def fast_string_to_time(string) if string =~ Format::ISO_DATETIME - microsec = ($7.to_f * 1_000_000).to_i + microsec = ($7.to_r * 1_000_000).to_i new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec end end diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb index 8491d42b86..dd40351a38 100644 --- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb @@ -1,3 +1,5 @@ +require 'uri' + module ActiveRecord module ConnectionAdapters class ConnectionSpecification #:nodoc: diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index 0b6734b010..6bf7af081f 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -215,7 +215,7 @@ module ActiveRecord def select_rows(sql, name = nil) @connection.query_with_result = true - rows = exec_without_stmt(sql, name).rows + rows = exec_query(sql, name).rows @connection.more_results && @connection.next_result # invoking stored procedures with CLIENT_MULTI_RESULTS requires this to tidy up else connection will be dropped rows end @@ -279,31 +279,164 @@ module ActiveRecord end def exec_query(sql, name = 'SQL', binds = []) - log(sql, name, binds) do - exec_stmt(sql, name, binds) do |cols, stmt| - ActiveRecord::Result.new(cols, stmt.to_a) if cols - end + # If the configuration sets prepared_statements:false, binds will + # always be empty, since the bind variables will have been already + # substituted and removed from binds by BindVisitor, so this will + # effectively disable prepared statement usage completely. + if binds.empty? + result_set, affected_rows = exec_without_stmt(sql, name) + else + result_set, affected_rows = exec_stmt(sql, name, binds) end + + yield affected_rows if block_given? + + result_set end def last_inserted_id(result) @connection.insert_id end + module Fields + class Type + def type; end + + def type_cast_for_write(value) + value + end + end + + class Identity < Type + def type_cast(value); value; end + end + + class Integer < Type + def type_cast(value) + return if value.nil? + + value.to_i rescue value ? 1 : 0 + end + end + + class Date < Type + def type; :date; end + + def type_cast(value) + return if value.nil? + + # FIXME: probably we can improve this since we know it is mysql + # specific + ConnectionAdapters::Column.value_to_date value + end + end + + class DateTime < Type + def type; :datetime; end + + def type_cast(value) + return if value.nil? + + # FIXME: probably we can improve this since we know it is mysql + # specific + ConnectionAdapters::Column.string_to_time value + end + end + + class Time < Type + def type; :time; end + + def type_cast(value) + return if value.nil? + + # FIXME: probably we can improve this since we know it is mysql + # specific + ConnectionAdapters::Column.string_to_dummy_time value + end + end + + class Float < Type + def type; :float; end + + def type_cast(value) + return if value.nil? + + value.to_f + end + end + + class Decimal < Type + def type_cast(value) + return if value.nil? + + ConnectionAdapters::Column.value_to_decimal value + end + end + + class Boolean < Type + def type_cast(value) + return if value.nil? + + ConnectionAdapters::Column.value_to_boolean value + end + end + + TYPES = {} + + # Register an MySQL +type_id+ with a typcasting object in + # +type+. + def self.register_type(type_id, type) + TYPES[type_id] = type + end + + def self.alias_type(new, old) + TYPES[new] = TYPES[old] + end + + register_type Mysql::Field::TYPE_TINY, Fields::Boolean.new + register_type Mysql::Field::TYPE_LONG, Fields::Integer.new + alias_type Mysql::Field::TYPE_LONGLONG, Mysql::Field::TYPE_LONG + alias_type Mysql::Field::TYPE_NEWDECIMAL, Mysql::Field::TYPE_LONG + + register_type Mysql::Field::TYPE_VAR_STRING, Fields::Identity.new + register_type Mysql::Field::TYPE_BLOB, Fields::Identity.new + register_type Mysql::Field::TYPE_DATE, Fields::Date.new + register_type Mysql::Field::TYPE_DATETIME, Fields::DateTime.new + register_type Mysql::Field::TYPE_TIME, Fields::Time.new + register_type Mysql::Field::TYPE_FLOAT, Fields::Float.new + + Mysql::Field.constants.grep(/TYPE/).map { |class_name| + Mysql::Field.const_get class_name + }.reject { |const| TYPES.key? const }.each do |const| + register_type const, Fields::Identity.new + end + end + 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. :'( log(sql, name) do result = @connection.query(sql) - cols = [] - rows = [] + affected_rows = @connection.affected_rows if result - cols = result.fetch_fields.map { |field| field.name } - rows = result.to_a + types = {} + result.fetch_fields.each { |field| + if field.decimals > 0 + types[field.name] = Fields::Decimal.new + else + types[field.name] = Fields::TYPES.fetch(field.type) { + Fields::Identity.new + } + end + } + result_set = ActiveRecord::Result.new(types.keys, result.to_a, types) result.free + else + result_set = ActiveRecord::Result.new([], []) end - ActiveRecord::Result.new(cols, rows) + + [result_set, affected_rows] end end @@ -321,16 +454,18 @@ module ActiveRecord alias :create :insert_sql def exec_delete(sql, name, binds) - log(sql, name, binds) do - exec_stmt(sql, name, binds) do |cols, stmt| - stmt.affected_rows - end + affected_rows = 0 + + exec_query(sql, name, binds) do |n| + affected_rows = n end + + affected_rows end alias :exec_update :exec_delete def begin_db_transaction #:nodoc: - exec_without_stmt "BEGIN" + exec_query "BEGIN" rescue Mysql::Error # Transactions aren't supported end @@ -339,41 +474,44 @@ module ActiveRecord def exec_stmt(sql, name, binds) cache = {} - if binds.empty? - stmt = @connection.prepare(sql) - else - cache = @statements[sql] ||= { - :stmt => @connection.prepare(sql) - } - stmt = cache[:stmt] - end + log(sql, name, binds) do + if binds.empty? + stmt = @connection.prepare(sql) + else + cache = @statements[sql] ||= { + :stmt => @connection.prepare(sql) + } + stmt = cache[:stmt] + end - begin - 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 - # need to close the statement and delete the statement from the - # cache. - stmt.close - @statements.delete sql - raise e - end + begin + 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 + # need to close the statement and delete the statement from the + # cache. + stmt.close + @statements.delete sql + raise e + end - cols = nil - if metadata = stmt.result_metadata - cols = cache[:cols] ||= metadata.fetch_fields.map { |field| - field.name - } - end + cols = nil + if metadata = stmt.result_metadata + cols = cache[:cols] ||= metadata.fetch_fields.map { |field| + field.name + } + end - result = yield [cols, stmt] + result_set = ActiveRecord::Result.new(cols, stmt.to_a) if cols + affected_rows = stmt.affected_rows - stmt.result_metadata.free if cols - stmt.free_result - stmt.close if binds.empty? + stmt.result_metadata.free if cols + stmt.free_result + stmt.close if binds.empty? - result + [result_set, affected_rows] + end end def connect diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb index df3d5e4657..6657491c06 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb @@ -223,6 +223,7 @@ module ActiveRecord alias_type 'bit', 'text' alias_type 'varbit', 'text' alias_type 'macaddr', 'text' + alias_type 'uuid', 'text' # FIXME: I don't think this is correct. We should probably be returning a parsed date, # but the tests pass with a string returned. diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 03c318f5f7..40cd65cce9 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -1,5 +1,4 @@ require 'active_record/connection_adapters/abstract_adapter' -require 'active_support/core_ext/object/blank' require 'active_record/connection_adapters/statement_pool' require 'active_record/connection_adapters/postgresql/oid' require 'arel/visitors/bind_visitor' @@ -89,7 +88,6 @@ module ActiveRecord else string end - end def cidr_to_string(object) @@ -188,6 +186,7 @@ module ActiveRecord case sql_type when /^bigint/i; 8 when /^smallint/i; 2 + when /^timestamp/i; nil else super end end @@ -202,6 +201,8 @@ module ActiveRecord def extract_precision(sql_type) if sql_type == 'money' self.class.money_precision + elsif sql_type =~ /timestamp/i + $1.to_i if sql_type =~ /\((\d+)\)/ else super end @@ -256,7 +257,7 @@ module ActiveRecord :integer # UUID type when 'uuid' - :string + :uuid # Small and big integer types when /^(?:small|big)int$/ :integer @@ -319,6 +320,10 @@ module ActiveRecord def macaddr(name, options = {}) column(name, 'macaddr', options) end + + def uuid(name, options = {}) + column(name, 'uuid', options) + end end ADAPTER_NAME = 'PostgreSQL' @@ -341,7 +346,8 @@ module ActiveRecord :hstore => { :name => "hstore" }, :inet => { :name => "inet" }, :cidr => { :name => "cidr" }, - :macaddr => { :name => "macaddr" } + :macaddr => { :name => "macaddr" }, + :uuid => { :name => "uuid" } } # Returns 'PostgreSQL' as adapter name for identification purposes. @@ -467,6 +473,7 @@ module ActiveRecord def reconnect! clear_cache! @connection.reset + @open_transactions = 0 configure_connection end @@ -796,13 +803,6 @@ module ActiveRecord Arel::Nodes::BindParam.new "$#{index + 1}" end - class Result < ActiveRecord::Result - def initialize(columns, rows, column_types) - super(columns, rows) - @column_types = column_types - end - end - def exec_query(sql, name = 'SQL', binds = []) log(sql, name, binds) do result = binds.empty? ? exec_no_cache(sql, binds) : @@ -818,7 +818,7 @@ module ActiveRecord } end - ret = Result.new(result.fields, result.values, types) + ret = ActiveRecord::Result.new(result.fields, result.values, types) result.clear return ret end @@ -909,7 +909,8 @@ module ActiveRecord end # Create a new PostgreSQL database. Options include <tt>:owner</tt>, <tt>:template</tt>, - # <tt>:encoding</tt>, <tt>:tablespace</tt>, and <tt>:connection_limit</tt> (note that MySQL uses + # <tt>:encoding</tt>, <tt>:collation</tt>, <tt>:ctype</tt>, + # <tt>:tablespace</tt>, and <tt>:connection_limit</tt> (note that MySQL uses # <tt>:charset</tt> while PostgreSQL uses <tt>:encoding</tt>). # # Example: @@ -926,6 +927,10 @@ module ActiveRecord " TEMPLATE = \"#{value}\"" when :encoding " ENCODING = '#{value}'" + when :collation + " LC_COLLATE = '#{value}'" + when :ctype + " LC_CTYPE = '#{value}'" when :tablespace " TABLESPACE = \"#{value}\"" when :connection_limit @@ -1052,6 +1057,20 @@ module ActiveRecord end_sql end + # Returns the current database collation. + def collation + query(<<-end_sql, 'SCHEMA')[0][0] + SELECT pg_database.datcollate FROM pg_database WHERE pg_database.datname LIKE '#{current_database}' + end_sql + end + + # Returns the current database ctype. + def ctype + query(<<-end_sql, 'SCHEMA')[0][0] + SELECT pg_database.datctype FROM pg_database WHERE pg_database.datname LIKE '#{current_database}' + end_sql + end + # Returns an array of schema names. def schema_names query(<<-SQL, 'SCHEMA').flatten @@ -1201,12 +1220,19 @@ module ActiveRecord end # Renames a table. + # Also renames a table's primary key sequence if the sequence name matches the + # Active Record default. # # Example: # rename_table('octopuses', 'octopi') def rename_table(name, new_name) clear_cache! execute "ALTER TABLE #{quote_table_name(name)} RENAME TO #{quote_table_name(new_name)}" + pk, seq = pk_and_sequence_for(new_name) + if seq == "#{name}_#{pk}_seq" + new_seq = "#{new_name}_#{pk}_seq" + execute "ALTER TABLE #{quote_table_name(seq)} RENAME TO #{quote_table_name(new_seq)}" + end end # Adds a new column to the named table. @@ -1281,6 +1307,13 @@ module ActiveRecord when 5..8; 'bigint' else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with precision 0 instead.") end + when 'datetime' + return super unless precision + + case precision + when 0..6; "timestamp(#{precision})" + else raise(ActiveRecordError, "No timestamp type has precision of #{precision}. The allowed range of precision is from 0 to 6") + end else super end @@ -1341,7 +1374,7 @@ module ActiveRecord UNIQUE_VIOLATION = "23505" def translate_exception(exception, message) - case exception.result.error_field(PGresult::PG_DIAG_SQLSTATE) + case exception.result.try(:error_field, PGresult::PG_DIAG_SQLSTATE) when UNIQUE_VIOLATION RecordNotUnique.new(message, exception) when FOREIGN_KEY_VIOLATION @@ -1446,7 +1479,7 @@ module ActiveRecord if @config[:encoding] @connection.set_client_encoding(@config[:encoding]) end - self.client_min_messages = @config[:min_messages] if @config[:min_messages] + self.client_min_messages = @config[:min_messages] || 'warning' self.schema_search_path = @config[:schema_search_path] || @config[:schema_order] # Use standard-conforming strings if available so we don't have to do the E'...' dance. diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index a0c7e559ce..4fe0013f0f 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -191,7 +191,7 @@ module ActiveRecord :decimal => { :name => "decimal" }, :datetime => { :name => "datetime" }, :timestamp => { :name => "datetime" }, - :time => { :name => "datetime" }, + :time => { :name => "time" }, :date => { :name => "date" }, :binary => { :name => "blob" }, :boolean => { :name => "boolean" } @@ -380,9 +380,9 @@ module ActiveRecord case field["dflt_value"] when /^null$/i field["dflt_value"] = nil - when /^'(.*)'$/ + when /^'(.*)'$/m field["dflt_value"] = $1.gsub("''", "'") - when /^"(.*)"$/ + when /^"(.*)"$/m field["dflt_value"] = $1.gsub('""', '"') end diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb index 7b218a5570..7863c795ed 100644 --- a/activerecord/lib/active_record/connection_handling.rb +++ b/activerecord/lib/active_record/connection_handling.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/module/delegation' module ActiveRecord module ConnectionHandling @@ -90,6 +89,10 @@ module ActiveRecord connection_handler.remove_connection(klass) end + def clear_cache! # :nodoc: + connection.schema_cache.clear! + end + delegate :clear_active_connections!, :clear_reloadable_connections!, :clear_all_connections!, :to => :connection_handler end diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 1fa6c701bb..aad21b8e37 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -1,90 +1,90 @@ -require 'active_support/concern' require 'active_support/core_ext/hash/indifferent_access' -require 'active_support/core_ext/object/deep_dup' +require 'active_support/core_ext/object/duplicable' require 'thread' module ActiveRecord - module Core - extend ActiveSupport::Concern + ActiveSupport.on_load(:active_record_config) do + ## + # :singleton-method: + # + # Accepts a logger conforming to the interface of Log4r which is then + # passed on to any new database connections made and which can be + # retrieved on both a class and instance level by calling +logger+. + mattr_accessor :logger, instance_accessor: false - included do - ## - # :singleton-method: - # - # Accepts a logger conforming to the interface of Log4r which is then - # passed on to any new database connections made and which can be - # retrieved on both a class and instance level by calling +logger+. - config_attribute :logger, :global => true + ## + # :singleton-method: + # Contains the database configuration - as is typically stored in config/database.yml - + # as a Hash. + # + # For example, the following database.yml... + # + # development: + # adapter: sqlite3 + # database: db/development.sqlite3 + # + # production: + # adapter: sqlite3 + # database: db/production.sqlite3 + # + # ...would result in ActiveRecord::Base.configurations to look like this: + # + # { + # 'development' => { + # 'adapter' => 'sqlite3', + # 'database' => 'db/development.sqlite3' + # }, + # 'production' => { + # 'adapter' => 'sqlite3', + # 'database' => 'db/production.sqlite3' + # } + # } + mattr_accessor :configurations, instance_accessor: false + self.configurations = {} - ## - # :singleton-method: - # Contains the database configuration - as is typically stored in config/database.yml - - # as a Hash. - # - # For example, the following database.yml... - # - # development: - # adapter: sqlite3 - # database: db/development.sqlite3 - # - # production: - # adapter: sqlite3 - # database: db/production.sqlite3 - # - # ...would result in ActiveRecord::Base.configurations to look like this: - # - # { - # 'development' => { - # 'adapter' => 'sqlite3', - # 'database' => 'db/development.sqlite3' - # }, - # 'production' => { - # 'adapter' => 'sqlite3', - # 'database' => 'db/production.sqlite3' - # } - # } - config_attribute :configurations, :global => true - self.configurations = {} + ## + # :singleton-method: + # Determines whether to use Time.utc (using :utc) or Time.local (using :local) when pulling + # dates and times from the database. This is set to :utc by default. + mattr_accessor :default_timezone, instance_accessor: false + self.default_timezone = :utc - ## - # :singleton-method: - # Determines whether to use Time.utc (using :utc) or Time.local (using :local) when pulling - # dates and times from the database. This is set to :utc by default. - config_attribute :default_timezone, :global => true - self.default_timezone = :utc + ## + # :singleton-method: + # Specifies the format to use when dumping the database schema with Rails' + # Rakefile. If :sql, the schema is dumped as (potentially database- + # specific) SQL statements. If :ruby, the schema is dumped as an + # ActiveRecord::Schema file which can be loaded into any database that + # supports migrations. Use :ruby if you want to have different database + # adapters for, e.g., your development and test environments. + mattr_accessor :schema_format, instance_accessor: false + self.schema_format = :ruby - ## - # :singleton-method: - # Specifies the format to use when dumping the database schema with Rails' - # Rakefile. If :sql, the schema is dumped as (potentially database- - # specific) SQL statements. If :ruby, the schema is dumped as an - # ActiveRecord::Schema file which can be loaded into any database that - # supports migrations. Use :ruby if you want to have different database - # adapters for, e.g., your development and test environments. - config_attribute :schema_format, :global => true - self.schema_format = :ruby + ## + # :singleton-method: + # Specify whether or not to use timestamps for migration versions + mattr_accessor :timestamped_migrations, instance_accessor: false + self.timestamped_migrations = true - ## - # :singleton-method: - # Specify whether or not to use timestamps for migration versions - config_attribute :timestamped_migrations, :global => true - self.timestamped_migrations = true + mattr_accessor :connection_handler, instance_accessor: false + self.connection_handler = ConnectionAdapters::ConnectionHandler.new + mattr_accessor :dependent_restrict_raises, instance_accessor: false + self.dependent_restrict_raises = true + end + + module Core + extend ActiveSupport::Concern + + included do ## # :singleton-method: # The connection handler config_attribute :connection_handler - self.connection_handler = ConnectionAdapters::ConnectionHandler.new - ## - # :singleton-method: - # Specifies whether or not has_many or has_one association option - # :dependent => :restrict raises an exception. If set to true, the - # ActiveRecord::DeleteRestrictionError exception will be raised - # along with a DEPRECATION WARNING. If set to false, an error would - # be added to the model instead. - config_attribute :dependent_restrict_raises, :global => true - self.dependent_restrict_raises = true + %w(logger configurations default_timezone schema_format timestamped_migrations).each do |name| + config_attribute name, global: true + end end module ClassMethods @@ -173,7 +173,10 @@ module ActiveRecord # # Instantiates a single new object bypassing mass-assignment security # User.new({ :first_name => 'Jamie', :is_admin => true }, :without_protection => true) def initialize(attributes = nil, options = {}) - @attributes = self.class.initialize_attributes(self.class.column_defaults.deep_dup) + defaults = self.class.column_defaults.dup + defaults.each { |k, v| defaults[k] = v.dup if v.duplicable? } + + @attributes = self.class.initialize_attributes(defaults) @columns_hash = self.class.column_types.dup init_internals @@ -185,7 +188,7 @@ module ActiveRecord assign_attributes(attributes, options) if attributes yield self if block_given? - run_callbacks :initialize if _initialize_callbacks.any? + run_callbacks :initialize unless _initialize_callbacks.empty? end # Initialize an empty model object from +coder+. +coder+ must contain @@ -370,7 +373,7 @@ module ActiveRecord # # 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/ + # See also http://tenderlovemaking.com/2011/06/28/til-its-ok-to-return-nil-from-to_ary.html def to_ary # :nodoc: nil end @@ -380,15 +383,17 @@ module ActiveRecord @attributes[pk] = nil unless @attributes.key?(pk) - @aggregation_cache = {} - @association_cache = {} - @attributes_cache = {} - @previously_changed = {} - @changed_attributes = {} - @readonly = false - @destroyed = false - @marked_for_destruction = false - @new_record = true + @aggregation_cache = {} + @association_cache = {} + @attributes_cache = {} + @previously_changed = {} + @changed_attributes = {} + @readonly = false + @destroyed = false + @marked_for_destruction = false + @new_record = true + @mass_assignment_options = nil + @_start_transaction_state = {} end end end diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb index b163ef3c12..c877079b25 100644 --- a/activerecord/lib/active_record/counter_cache.rb +++ b/activerecord/lib/active_record/counter_cache.rb @@ -1,111 +1,115 @@ 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 - # counter has been corrupted or modified directly by SQL. - # - # ==== Parameters - # - # * +id+ - The id of the object you wish to reset a counter on. - # * +counters+ - One or more counter names to reset - # - # ==== Examples - # - # # For Post with id #1 records reset the comments_count - # Post.reset_counters(1, :comments) - def reset_counters(id, *counters) - object = find(id) - counters.each do |association| - has_many_association = reflect_on_association(association.to_sym) + extend ActiveSupport::Concern - foreign_key = has_many_association.foreign_key.to_s - child_class = has_many_association.klass - belongs_to = child_class.reflect_on_all_associations(:belongs_to) - reflection = belongs_to.find { |e| e.foreign_key.to_s == foreign_key } - counter_name = reflection.counter_cache_column + module ClassMethods + # 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 + # counter has been corrupted or modified directly by SQL. + # + # ==== Parameters + # + # * +id+ - The id of the object you wish to reset a counter on. + # * +counters+ - One or more counter names to reset + # + # ==== Examples + # + # # For Post with id #1 records reset the comments_count + # Post.reset_counters(1, :comments) + def reset_counters(id, *counters) + object = find(id) + counters.each do |association| + has_many_association = reflect_on_association(association.to_sym) - stmt = unscoped.where(arel_table[primary_key].eq(object.id)).arel.compile_update({ - arel_table[counter_name] => object.send(association).count - }) - connection.update stmt - end - return true - end + foreign_key = has_many_association.foreign_key.to_s + child_class = has_many_association.klass + belongs_to = child_class.reflect_on_all_associations(:belongs_to) + reflection = belongs_to.find { |e| e.foreign_key.to_s == foreign_key && e.options[:counter_cache].present? } + counter_name = reflection.counter_cache_column - # A generic "counter updater" implementation, intended primarily to be - # used by increment_counter and decrement_counter, but which may also - # be useful on its own. It simply does a direct SQL update for the record - # with the given ID, altering the given hash of counters by the amount - # given by the corresponding value: - # - # ==== Parameters - # - # * +id+ - The id of the object you wish to update a counter on or an Array of ids. - # * +counters+ - An Array of Hashes containing the names of the fields - # to update as keys and the amount to update the field by as values. - # - # ==== Examples - # - # # For the Post with id of 5, decrement the comment_count by 1, and - # # increment the action_count by 1 - # Post.update_counters 5, :comment_count => -1, :action_count => 1 - # # Executes the following SQL: - # # UPDATE posts - # # SET comment_count = COALESCE(comment_count, 0) - 1, - # # action_count = COALESCE(action_count, 0) + 1 - # # WHERE id = 5 - # - # # For the Posts with id of 10 and 15, increment the comment_count by 1 - # Post.update_counters [10, 15], :comment_count => 1 - # # Executes the following SQL: - # # UPDATE posts - # # SET comment_count = COALESCE(comment_count, 0) + 1 - # # WHERE id IN (10, 15) - def update_counters(id, counters) - updates = counters.map do |counter_name, value| - operator = value < 0 ? '-' : '+' - quoted_column = connection.quote_column_name(counter_name) - "#{quoted_column} = COALESCE(#{quoted_column}, 0) #{operator} #{value.abs}" + stmt = unscoped.where(arel_table[primary_key].eq(object.id)).arel.compile_update({ + arel_table[counter_name] => object.send(association).count + }) + connection.update stmt + end + return true end - where(primary_key => id).update_all updates.join(', ') - end + # A generic "counter updater" implementation, intended primarily to be + # used by increment_counter and decrement_counter, but which may also + # be useful on its own. It simply does a direct SQL update for the record + # with the given ID, altering the given hash of counters by the amount + # given by the corresponding value: + # + # ==== Parameters + # + # * +id+ - The id of the object you wish to update a counter on or an Array of ids. + # * +counters+ - An Array of Hashes containing the names of the fields + # to update as keys and the amount to update the field by as values. + # + # ==== Examples + # + # # For the Post with id of 5, decrement the comment_count by 1, and + # # increment the action_count by 1 + # Post.update_counters 5, :comment_count => -1, :action_count => 1 + # # Executes the following SQL: + # # UPDATE posts + # # SET comment_count = COALESCE(comment_count, 0) - 1, + # # action_count = COALESCE(action_count, 0) + 1 + # # WHERE id = 5 + # + # # For the Posts with id of 10 and 15, increment the comment_count by 1 + # Post.update_counters [10, 15], :comment_count => 1 + # # Executes the following SQL: + # # UPDATE posts + # # SET comment_count = COALESCE(comment_count, 0) + 1 + # # WHERE id IN (10, 15) + def update_counters(id, counters) + updates = counters.map do |counter_name, value| + operator = value < 0 ? '-' : '+' + quoted_column = connection.quote_column_name(counter_name) + "#{quoted_column} = COALESCE(#{quoted_column}, 0) #{operator} #{value.abs}" + end - # Increment a number field by one, usually representing a count. - # - # This is used for caching aggregate values, so that they don't need to be computed every time. - # For example, a DiscussionBoard may cache post_count and comment_count otherwise every time the board is - # shown it would have to run an SQL query to find how many posts and comments there are. - # - # ==== Parameters - # - # * +counter_name+ - The name of the field that should be incremented. - # * +id+ - The id of the object that should be incremented. - # - # ==== Examples - # - # # Increment the post_count column for the record with an id of 5 - # DiscussionBoard.increment_counter(:post_count, 5) - def increment_counter(counter_name, id) - update_counters(id, counter_name => 1) - end + where(primary_key => id).update_all updates.join(', ') + end + + # Increment a number field by one, usually representing a count. + # + # This is used for caching aggregate values, so that they don't need to be computed every time. + # For example, a DiscussionBoard may cache post_count and comment_count otherwise every time the board is + # shown it would have to run an SQL query to find how many posts and comments there are. + # + # ==== Parameters + # + # * +counter_name+ - The name of the field that should be incremented. + # * +id+ - The id of the object that should be incremented. + # + # ==== Examples + # + # # Increment the post_count column for the record with an id of 5 + # DiscussionBoard.increment_counter(:post_count, 5) + def increment_counter(counter_name, id) + update_counters(id, counter_name => 1) + end - # Decrement a number field by one, usually representing a count. - # - # This works the same as increment_counter but reduces the column value by 1 instead of increasing it. - # - # ==== Parameters - # - # * +counter_name+ - The name of the field that should be decremented. - # * +id+ - The id of the object that should be decremented. - # - # ==== Examples - # - # # Decrement the post_count column for the record with an id of 5 - # DiscussionBoard.decrement_counter(:post_count, 5) - def decrement_counter(counter_name, id) - update_counters(id, counter_name => -1) + # Decrement a number field by one, usually representing a count. + # + # This works the same as increment_counter but reduces the column value by 1 instead of increasing it. + # + # ==== Parameters + # + # * +counter_name+ - The name of the field that should be decremented. + # * +id+ - The id of the object that should be decremented. + # + # ==== Examples + # + # # Decrement the post_count column for the record with an id of 5 + # DiscussionBoard.decrement_counter(:post_count, 5) + def decrement_counter(counter_name, id) + update_counters(id, counter_name => -1) + end end end end diff --git a/activerecord/lib/active_record/dynamic_matchers.rb b/activerecord/lib/active_record/dynamic_matchers.rb index e278e62ce7..3bac31c6aa 100644 --- a/activerecord/lib/active_record/dynamic_matchers.rb +++ b/activerecord/lib/active_record/dynamic_matchers.rb @@ -1,8 +1,8 @@ module ActiveRecord module DynamicMatchers #:nodoc: # This code in this file seems to have a lot of indirection, but the indirection - # is there to provide extension points for the active_record_deprecated_finders - # gem. When we stop supporting active_record_deprecated_finders (from Rails 5), + # is there to provide extension points for the activerecord-deprecated_finders + # gem. When we stop supporting activerecord-deprecated_finders (from Rails 5), # then we can remove the indirection. def respond_to?(name, include_private = false) @@ -53,6 +53,7 @@ module ActiveRecord @model = model @name = name.to_s @attribute_names = @name.match(self.class.pattern)[1].split('_and_') + @attribute_names.map! { |n| @model.attribute_aliases[n] || n } end def valid? @@ -73,17 +74,17 @@ module ActiveRecord end module Finder - # Extended in active_record_deprecated_finders + # Extended in activerecord-deprecated_finders def body result end - # Extended in active_record_deprecated_finders + # Extended in activerecord-deprecated_finders def result "#{finder}(#{attributes_hash})" end - # Extended in active_record_deprecated_finders + # Extended in activerecord-deprecated_finders def signature attribute_names.join(', ') end diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb index fc80f3081e..5f157fde6d 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -53,6 +53,10 @@ module ActiveRecord class RecordNotSaved < ActiveRecordError end + # Raised by ActiveRecord::Base.destroy! when a call to destroy would return false. + class RecordNotDestroyed < ActiveRecordError + end + # Raised when SQL statement cannot be executed by the database (for example, it's often the case for # MySQL when Ruby driver used is too old). class StatementInvalid < ActiveRecordError @@ -102,13 +106,11 @@ module ActiveRecord attr_reader :record, :attempted_action def initialize(record, attempted_action) + super("Attempted to #{attempted_action} a stale object: #{record.class.name}") @record = record @attempted_action = attempted_action end - def message - "Attempted to #{attempted_action} a stale object: #{record.class.name}" - end end # Raised when association is being configured improperly or @@ -164,9 +166,9 @@ module ActiveRecord class AttributeAssignmentError < ActiveRecordError attr_reader :exception, :attribute def initialize(message, exception, attribute) + super(message) @exception = exception @attribute = attribute - @message = message end end @@ -185,11 +187,12 @@ module ActiveRecord attr_reader :model def initialize(model) + super("Unknown primary key for table #{model.table_name} in model #{model}.") @model = model end - def message - "Unknown primary key for table #{model.table_name} in model #{model}." - end + end + + class ImmutableRelation < ActiveRecordError end end diff --git a/activerecord/lib/active_record/explain.rb b/activerecord/lib/active_record/explain.rb index b0eda8ef34..9e0390bed1 100644 --- a/activerecord/lib/active_record/explain.rb +++ b/activerecord/lib/active_record/explain.rb @@ -1,12 +1,12 @@ -require 'active_support/core_ext/class/attribute' +require 'active_support/lazy_load_hooks' module ActiveRecord + ActiveSupport.on_load(:active_record_config) do + mattr_accessor :auto_explain_threshold_in_seconds, instance_accessor: false + end + module Explain - def self.extended(base) - # If a query takes longer than these many seconds we log its query plan - # automatically. nil disables this feature. - base.config_attribute :auto_explain_threshold_in_seconds, :global => true - end + delegate :auto_explain_threshold_in_seconds, :auto_explain_threshold_in_seconds=, to: 'ActiveRecord::Model' # If auto explain is enabled, this method triggers EXPLAIN logging for the # queries triggered by the block if it takes more than the threshold as a diff --git a/activerecord/lib/active_record/explain_subscriber.rb b/activerecord/lib/active_record/explain_subscriber.rb index 1f8c4fc203..d5ba343b4c 100644 --- a/activerecord/lib/active_record/explain_subscriber.rb +++ b/activerecord/lib/active_record/explain_subscriber.rb @@ -2,9 +2,12 @@ require 'active_support/notifications' module ActiveRecord class ExplainSubscriber # :nodoc: - def call(*args) + def start(name, id, payload) + # unused + end + + def finish(name, id, payload) if queries = Thread.current[:available_queries_for_explain] - payload = args.last queries << payload.values_at(:sql, :binds) unless ignore_payload?(payload) end end diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 7e6512501c..e19ff5edd2 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -2,7 +2,6 @@ require 'erb' require 'yaml' require 'zlib' require 'active_support/dependencies' -require 'active_support/core_ext/object/blank' require 'active_record/fixtures/file' require 'active_record/errors' @@ -594,7 +593,7 @@ module ActiveRecord when :has_and_belongs_to_many if (targets = row.delete(association.name.to_s)) targets = targets.is_a?(Array) ? targets : targets.split(/\s*,\s*/) - table_name = association.options[:join_table] + table_name = association.join_table rows[table_name].concat targets.map { |target| { association.foreign_key => row[primary_key_name], association.association_foreign_key => ActiveRecord::Fixtures.identify(target) } diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb index 46d253b0a7..04fff99a6e 100644 --- a/activerecord/lib/active_record/inheritance.rb +++ b/activerecord/lib/active_record/inheritance.rb @@ -1,13 +1,16 @@ -require 'active_support/concern' module ActiveRecord + ActiveSupport.on_load(:active_record_config) do + # Determine whether to store the full constant name including namespace when using STI + mattr_accessor :store_full_sti_class, instance_accessor: false + self.store_full_sti_class = true + end + module Inheritance extend ActiveSupport::Concern included do - # Determine whether to store the full constant name including namespace when using STI config_attribute :store_full_sti_class - self.store_full_sti_class = true end module ClassMethods @@ -37,14 +40,26 @@ module ActiveRecord @symbolized_sti_name ||= sti_name.present? ? sti_name.to_sym : symbolized_base_class end - # Returns the base AR subclass that this class descends from. If A - # extends AR::Base, A.base_class will return A. If B descends from A + # Returns the class descending directly from ActiveRecord::Base (or + # that includes ActiveRecord::Model), or an abstract class, if any, in + # the inheritance hierarchy. + # + # If A extends AR::Base, A.base_class will return A. If B descends from A # through some arbitrarily deep hierarchy, B.base_class will return A. # # If B < A and C < B and if A is an abstract_class then both B.base_class # and C.base_class would return B as the answer since A is an abstract_class. def base_class - class_of_active_record_descendant(self) + unless self < Model::Tag + raise ActiveRecordError, "#{name} doesn't belong in a hierarchy descending from ActiveRecord" + end + + sup = active_record_super + if sup == Base || sup == Model || sup.abstract_class? + self + else + sup.base_class + end end # Set this to true if this is an abstract class (see <tt>abstract_class?</tt>). @@ -92,21 +107,6 @@ module ActiveRecord protected - # Returns the class descending directly from ActiveRecord::Base or an - # abstract class, if any, in the inheritance hierarchy. - def class_of_active_record_descendant(klass) - unless klass < Model - raise ActiveRecordError, "#{name} doesn't belong in a hierarchy descending from ActiveRecord" - end - - sup = klass.active_record_super - if [Base, Model].include?(klass) || [Base, Model].include?(sup) || sup.abstract_class? - klass - else - class_of_active_record_descendant(sup) - end - end - # Returns the class type of the record using the current module as a prefix. So descendants of # MyApp::Business::Account would appear as MyApp::Business::AccountSubclass. def compute_type(type_name) diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index 05e052b953..e96ed00f9c 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -1,4 +1,9 @@ module ActiveRecord + ActiveSupport.on_load(:active_record_config) do + mattr_accessor :lock_optimistically, instance_accessor: false + self.lock_optimistically = true + end + module Locking # == What is Optimistic Locking # @@ -51,8 +56,7 @@ module ActiveRecord extend ActiveSupport::Concern included do - config_attribute :lock_optimistically, :global => true - self.lock_optimistically = true + config_attribute :lock_optimistically end def locking_enabled? #:nodoc: @@ -82,7 +86,7 @@ module ActiveRecord stmt = relation.where( relation.table[self.class.primary_key].eq(id).and( - relation.table[lock_col].eq(quote_value(previous_lock_value)) + relation.table[lock_col].eq(self.class.quote_value(previous_lock_value)) ) ).arel.compile_update(arel_attributes_with_values_for_update(attribute_names)) @@ -164,16 +168,16 @@ module ActiveRecord super end - # If the locking column has no default value set, - # start the lock version at zero. Note we can't use - # <tt>locking_enabled?</tt> at this point as - # <tt>@attributes</tt> may not have been initialized yet. - def initialize_attributes(attributes, options = {}) #:nodoc: - if attributes.key?(locking_column) && lock_optimistically - attributes[locking_column] ||= 0 - end + def column_defaults + @column_defaults ||= begin + defaults = super - attributes + if defaults.key?(locking_column) && lock_optimistically + defaults[locking_column] ||= 0 + end + + defaults + end end end end diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index ac4f53c774..703265c334 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -1,6 +1,4 @@ -require "active_support/core_ext/module/delegation" require "active_support/core_ext/class/attribute_accessors" -require 'active_support/deprecation' require 'set' module ActiveRecord @@ -32,6 +30,12 @@ module ActiveRecord end end + class PendingMigrationError < ActiveRecordError#:nodoc: + def initialize + super("Migrations are pending run 'rake db:migrate RAILS_ENV=#{ENV['RAILS_ENV']}' to resolve the issue") + end + end + # = Active Record Migrations # # Migrations can manage the evolution of a schema used by several physical @@ -46,7 +50,7 @@ module ActiveRecord # # class AddSsl < ActiveRecord::Migration # def up - # add_column :accounts, :ssl_enabled, :boolean, :default => 1 + # add_column :accounts, :ssl_enabled, :boolean, :default => true # end # # def down @@ -232,7 +236,7 @@ module ActiveRecord # add_column :people, :salary, :integer # Person.reset_column_information # Person.all.each do |p| - # p.update_attribute :salary, SalaryCalculator.compute(p) + # p.update_column :salary, SalaryCalculator.compute(p) # end # end # end @@ -252,7 +256,7 @@ module ActiveRecord # ... # say_with_time "Updating salaries..." do # Person.all.each do |p| - # p.update_attribute :salary, SalaryCalculator.compute(p) + # p.update_column :salary, SalaryCalculator.compute(p) # end # end # ... @@ -326,10 +330,28 @@ module ActiveRecord class Migration autoload :CommandRecorder, 'active_record/migration/command_recorder' + + # This class is used to verify that all migrations have been run before + # loading a web page if config.active_record.migration_error is set to :page_load + class CheckPending + def initialize(app) + @app = app + end + + def call(env) + ActiveRecord::Migration.check_pending! + @app.call(env) + end + end + class << self attr_accessor :delegate # :nodoc: end + def self.check_pending! + raise ActiveRecord::PendingMigrationError if ActiveRecord::Migrator.needs_migration? + end + def self.method_missing(name, *args, &block) # :nodoc: (delegate || superclass.delegate).send(name, *args, &block) end @@ -605,6 +627,14 @@ module ActiveRecord end end + def needs_migration? + current_version < last_version + end + + def last_version + migrations(migrations_paths).last.try(:version)||0 + end + def proper_table_name(name) # Use the Active Record objects own table_name, or pre/suffix from ActiveRecord::Base if name is a symbol/string name.table_name rescue "#{ActiveRecord::Base.table_name_prefix}#{name}#{ActiveRecord::Base.table_name_suffix}" diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb index 96b62fdd61..95f4360578 100644 --- a/activerecord/lib/active_record/migration/command_recorder.rb +++ b/activerecord/lib/active_record/migration/command_recorder.rb @@ -51,13 +51,15 @@ module ActiveRecord super || delegate.respond_to?(*args) end - [:create_table, :create_join_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| + [:create_table, :create_join_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, :add_reference, :remove_reference].each do |method| class_eval <<-EOV, __FILE__, __LINE__ + 1 def #{method}(*args) # def create_table(*args) record(:"#{method}", args) # record(:create_table, args) end # end EOV end + alias :add_belongs_to :add_reference + alias :remove_belongs_to :remove_reference private @@ -102,6 +104,16 @@ module ActiveRecord [:remove_timestamps, args] end + def invert_add_reference(args) + [:remove_reference, args] + end + alias :invert_add_belongs_to :invert_add_reference + + def invert_remove_reference(args) + [:add_reference, args] + end + alias :invert_remove_belongs_to :invert_remove_reference + # Forwards any missing method call to the \target. def method_missing(method, *args, &block) @delegate.send(method, *args, &block) diff --git a/activerecord/lib/active_record/migration/join_table.rb b/activerecord/lib/active_record/migration/join_table.rb index 01a580781b..e880ae97bb 100644 --- a/activerecord/lib/active_record/migration/join_table.rb +++ b/activerecord/lib/active_record/migration/join_table.rb @@ -4,13 +4,11 @@ module ActiveRecord private def find_join_table_name(table_1, table_2, options = {}) - options.delete(:table_name) { join_table_name(table_1, table_2) } + options.delete(:table_name) || join_table_name(table_1, table_2) end def join_table_name(table_1, table_2) - tables_names = [table_1, table_2].map(&:to_s).sort - - tables_names.join("_").to_sym + [table_1, table_2].sort.join("_").to_sym end end end diff --git a/activerecord/lib/active_record/model.rb b/activerecord/lib/active_record/model.rb index 105d1e0e2b..57553c29eb 100644 --- a/activerecord/lib/active_record/model.rb +++ b/activerecord/lib/active_record/model.rb @@ -1,6 +1,31 @@ -require 'active_support/deprecation' +require 'active_support/core_ext/module/attribute_accessors' module ActiveRecord + module Configuration # :nodoc: + # This just abstracts out how we define configuration options in AR. Essentially we + # have mattr_accessors on the ActiveRecord:Model constant that define global defaults. + # Classes that then use AR get class_attributes defined, which means that when they + # are assigned the default will be overridden for that class and subclasses. (Except + # when options[:global] == true, in which case there is one global value always.) + def config_attribute(name, options = {}) + if options[:global] + class_eval <<-CODE, __FILE__, __LINE__ + 1 + def self.#{name}; ActiveRecord::Model.#{name}; end + def #{name}; ActiveRecord::Model.#{name}; end + def self.#{name}=(val); ActiveRecord::Model.#{name} = val; end + CODE + else + options[:instance_writer] ||= false + class_attribute name, options + + singleton_class.class_eval <<-CODE, __FILE__, __LINE__ + 1 + remove_method :#{name} + def #{name}; ActiveRecord::Model.#{name}; end + CODE + end + end + end + # <tt>ActiveRecord::Model</tt> can be included into a class to add Active Record persistence. # This is an alternative to inheriting from <tt>ActiveRecord::Base</tt>. Example: # @@ -9,41 +34,35 @@ module ActiveRecord # end # module Model - module ClassMethods #:nodoc: - include ActiveSupport::Callbacks::ClassMethods - include ActiveModel::Naming - include QueryCache::ClassMethods - include ActiveSupport::Benchmarkable - include ActiveSupport::DescendantsTracker - - include Querying - include Translation - include DynamicMatchers - include CounterCache - include Explain - include ConnectionHandling - end + extend ActiveSupport::Concern + extend ConnectionHandling + extend ActiveModel::Observing::ClassMethods - def self.included(base) - return if base.singleton_class < ClassMethods + # This allows us to detect an ActiveRecord::Model while it's in the process of being included. + module Tag; end + def self.append_features(base) base.class_eval do - extend ClassMethods - Callbacks::Register.setup(self) - initialize_generated_modules unless self == Base + include Tag + extend Configuration end + + super end - extend ActiveModel::Configuration - extend ActiveModel::Callbacks - extend ActiveModel::MassAssignmentSecurity::ClassMethods - extend ActiveModel::AttributeMethods::ClassMethods - extend Callbacks::Register - extend Explain - extend ConnectionHandling + included do + extend ActiveModel::Naming + extend ActiveSupport::Benchmarkable + extend ActiveSupport::DescendantsTracker + + extend QueryCache::ClassMethods + extend Querying + extend Translation + extend DynamicMatchers + extend Explain + extend ConnectionHandling - def self.extend(*modules) - ClassMethods.send(:include, *modules) + initialize_generated_modules unless self == Base end include Persistence @@ -52,17 +71,26 @@ module ActiveRecord include Inheritance include Scoping include Sanitization - include Integration include AttributeAssignment include ActiveModel::Conversion + include Integration include Validations - include Locking::Optimistic, Locking::Pessimistic + include CounterCache + include Locking::Optimistic + include Locking::Pessimistic include AttributeMethods - include Callbacks, ActiveModel::Observing, Timestamp + include Callbacks + include ActiveModel::Observing + include Timestamp include Associations include ActiveModel::SecurePassword - include AutosaveAssociation, NestedAttributes - include Aggregations, Transactions, Reflection, Serialization, Store + include AutosaveAssociation + include NestedAttributes + include Aggregations + include Transactions + include Reflection + include Serialization + include Store include Core class << self @@ -73,36 +101,60 @@ module ActiveRecord def abstract_class? false end - + + # Defines the name of the table column which will store the class name on single-table + # inheritance situations. def inheritance_column 'type' end end - module DeprecationProxy #:nodoc: - class << self - instance_methods.each { |m| undef_method m unless m =~ /^__|^object_id$|^instance_eval$/ } - - def method_missing(name, *args, &block) - if Model.respond_to?(name) - Model.send(name, *args, &block) - else - ActiveSupport::Deprecation.warn( - "The object passed to the active_record load hook was previously ActiveRecord::Base " \ - "(a Class). Now it is ActiveRecord::Model (a Module). You have called `#{name}' which " \ - "is only defined on ActiveRecord::Base. Please change your code so that it works with " \ - "a module rather than a class. (Model is included in Base, so anything added to Model " \ - "will be available on Base as well.)" - ) - Base.send(name, *args, &block) - end + class DeprecationProxy < BasicObject #:nodoc: + def initialize(model = Model, base = Base) + @model = model + @base = base + end + + def method_missing(name, *args, &block) + if @model.respond_to?(name, true) + @model.send(name, *args, &block) + else + ::ActiveSupport::Deprecation.warn( + "The object passed to the active_record load hook was previously ActiveRecord::Base " \ + "(a Class). Now it is ActiveRecord::Model (a Module). You have called `#{name}' which " \ + "is only defined on ActiveRecord::Base. Please change your code so that it works with " \ + "a module rather than a class. (Model is included in Base, so anything added to Model " \ + "will be available on Base as well.)" + ) + @base.send(name, *args, &block) end + end + + alias send method_missing - alias send method_missing + def extend(*mods) + ::ActiveSupport::Deprecation.warn( + "The object passed to the active_record load hook was previously ActiveRecord::Base " \ + "(a Class). Now it is ActiveRecord::Model (a Module). You have called `extend' which " \ + "would add singleton methods to Model. This is presumably not what you want, since the " \ + "methods would not be inherited down to Base. Rather than using extend, please use " \ + "ActiveSupport::Concern + include, which will ensure that your class methods are " \ + "inherited." + ) + @base.extend(*mods) end end end + # This hook is where config accessors on Model get defined. + # + # We don't want to just open the Model module and add stuff to it in other files, because + # that would cause Model to load, which causes all sorts of loading order issues. + # + # We need this hook rather than just using the :active_record one, because users of the + # :active_record hook may need to use config options. + ActiveSupport.run_load_hooks(:active_record_config, Model) + # Load Base at this point, because the active_record load hook is run in that file. Base end diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index 7f38dda11e..99de16cd33 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -1,7 +1,18 @@ -require 'active_support/concern' -require 'active_support/core_ext/class/attribute_accessors' module ActiveRecord + ActiveSupport.on_load(:active_record_config) do + mattr_accessor :primary_key_prefix_type, instance_accessor: false + + mattr_accessor :table_name_prefix, instance_accessor: false + self.table_name_prefix = "" + + mattr_accessor :table_name_suffix, instance_accessor: false + self.table_name_suffix = "" + + mattr_accessor :pluralize_table_names, instance_accessor: false + self.pluralize_table_names = true + end + module ModelSchema extend ActiveSupport::Concern @@ -13,7 +24,7 @@ module ActiveRecord # the Product class will look for "productid" instead of "id" as the primary column. If the # latter is specified, the Product class will look for "product_id" instead of "id". Remember # that this is a global setting for all Active Records. - config_attribute :primary_key_prefix_type, :global => true + config_attribute :primary_key_prefix_type, global: true ## # :singleton-method: @@ -26,14 +37,12 @@ module ActiveRecord # a namespace by defining a singleton method in the parent module called table_name_prefix which # returns your chosen prefix. config_attribute :table_name_prefix - self.table_name_prefix = "" ## # :singleton-method: # Works like +table_name_prefix+, but appends instead of prepends (set to "_basecamp" gives "projects_basecamp", # "people_basecamp"). By default, the suffix is the empty string. config_attribute :table_name_suffix - self.table_name_suffix = "" ## # :singleton-method: @@ -41,7 +50,6 @@ module ActiveRecord # If true, the default table name for a Product class will be +products+. If false, it would just be +product+. # See table_name for the full rules on table/class naming. This is true, by default. config_attribute :pluralize_table_names - self.pluralize_table_names = true end module ClassMethods @@ -135,16 +143,12 @@ module ActiveRecord # Computes the table name, (re)sets it internally, and returns it. def reset_table_name #:nodoc: - if abstract_class? - self.table_name = if active_record_super == Base || active_record_super.abstract_class? - nil - else - active_record_super.table_name - end + self.table_name = if abstract_class? + active_record_super == Base ? nil : active_record_super.table_name elsif active_record_super.abstract_class? - self.table_name = active_record_super.table_name || compute_table_name + active_record_super.table_name || compute_table_name else - self.table_name = compute_table_name + compute_table_name end end @@ -221,7 +225,7 @@ module ActiveRecord def decorate_columns(columns_hash) # :nodoc: return if columns_hash.empty? - serialized_attributes.keys.each do |key| + serialized_attributes.each_key do |key| columns_hash[key] = AttributeMethods::Serialization::Type.new(columns_hash[key]) end @@ -255,13 +259,12 @@ module ActiveRecord # and true as the value. This makes it possible to do O(1) lookups in respond_to? to check if a given method for attribute # is available. def column_methods_hash #:nodoc: - @dynamic_methods_hash ||= column_names.inject(Hash.new(false)) do |methods, attr| + @dynamic_methods_hash ||= column_names.each_with_object(Hash.new(false)) do |attr, methods| attr_name = attr.to_s methods[attr.to_sym] = attr_name methods["#{attr}=".to_sym] = attr_name methods["#{attr}?".to_sym] = attr_name methods["#{attr}_before_type_cast".to_sym] = attr_name - methods end end @@ -308,8 +311,11 @@ module ActiveRecord @relation = nil end - def clear_cache! # :nodoc: - connection.schema_cache.clear! + # This is a hook for use by modules that need to do extra stuff to + # attributes when they are initialized. (e.g. attribute + # serialization) + def initialize_attributes(attributes, options = {}) #:nodoc: + attributes end private @@ -317,8 +323,7 @@ module ActiveRecord # Guesses the table name, but does not decorate it with prefix and suffix information. def undecorated_table_name(class_name = base_class.name) table_name = class_name.to_s.demodulize.underscore - table_name = table_name.pluralize if pluralize_table_names - table_name + pluralize_table_names ? table_name.pluralize : table_name end # Computes and returns a table name according to default conventions. diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index 95a2ddcc11..be013a068c 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -1,10 +1,13 @@ require 'active_support/core_ext/hash/except' require 'active_support/core_ext/object/try' -require 'active_support/core_ext/object/blank' require 'active_support/core_ext/hash/indifferent_access' -require 'active_support/core_ext/class/attribute' module ActiveRecord + ActiveSupport.on_load(:active_record_config) do + mattr_accessor :nested_attributes_options, instance_accessor: false + self.nested_attributes_options = {} + end + module NestedAttributes #:nodoc: class TooManyRecords < ActiveRecordError end @@ -13,7 +16,6 @@ module ActiveRecord included do config_attribute :nested_attributes_options - self.nested_attributes_options = {} end # = Active Record Nested Attributes @@ -347,7 +349,7 @@ module ActiveRecord if respond_to?(method) 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?" + raise ArgumentError, "Cannot build association `#{association_name}'. Are you trying to build a polymorphic one-to-one association?" end end end @@ -369,7 +371,7 @@ module ActiveRecord # }) # # Will update the name of the Person with ID 1, build a new associated - # person with the name `John', and mark the associated Person with ID 2 + # person with the name 'John', and mark the associated Person with ID 2 # for destruction. # # Also accepts an Array of attribute hashes: @@ -405,7 +407,7 @@ module ActiveRecord association.target else attribute_ids = attributes_collection.map {|a| a['id'] || a[:id] }.compact - attribute_ids.empty? ? [] : association.scoped.where(association.klass.primary_key => attribute_ids) + attribute_ids.empty? ? [] : association.scope.where(association.klass.primary_key => attribute_ids) end attributes_collection.each do |attributes| diff --git a/activerecord/lib/active_record/null_relation.rb b/activerecord/lib/active_record/null_relation.rb index aca8291d75..4c1c91e3df 100644 --- a/activerecord/lib/active_record/null_relation.rb +++ b/activerecord/lib/active_record/null_relation.rb @@ -1,8 +1,7 @@ # -*- coding: utf-8 -*- module ActiveRecord - # = Active Record Null Relation - module NullRelation + module NullRelation # :nodoc: def exec_queries @records = [] end diff --git a/activerecord/lib/active_record/observer.rb b/activerecord/lib/active_record/observer.rb index fdf17c003c..6b2f6f98a5 100644 --- a/activerecord/lib/active_record/observer.rb +++ b/activerecord/lib/active_record/observer.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/class/attribute' module ActiveRecord # = Active Record Observer @@ -74,6 +73,12 @@ module ActiveRecord # # Observers will not be invoked unless you define these in your application configuration. # + # If you are using Active Record outside Rails, activate the observers explicitly in a configuration or + # environment file: + # + # ActiveRecord::Base.add_observer CommentObserver.instance + # ActiveRecord::Base.add_observer SignupObserver.instance + # # == Loading # # Observers register themselves in the model class they observe, since it is the class that diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index a1bc39a32d..6b4b9bd103 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -1,4 +1,3 @@ -require 'active_support/concern' module ActiveRecord # = Active Record Persistence @@ -122,6 +121,11 @@ module ActiveRecord # Deletes the record in the database and freezes this instance to reflect # that no changes should be made (since they can't be persisted). + # + # There's a series of callbacks associated with <tt>destroy</tt>. If + # the <tt>before_destroy</tt> callback return +false+ the action is cancelled + # and <tt>destroy</tt> returns +false+. See + # ActiveRecord::Callbacks for further details. def destroy raise ReadOnlyRecord if readonly? destroy_associations @@ -130,6 +134,17 @@ module ActiveRecord freeze end + # Deletes the record in the database and freezes this instance to reflect + # that no changes should be made (since they can't be persisted). + # + # There's a series of callbacks associated with <tt>destroy!</tt>. If + # the <tt>before_destroy</tt> callback return +false+ the action is cancelled + # and <tt>destroy!</tt> raises ActiveRecord::RecordNotDestroyed. See + # ActiveRecord::Callbacks for further details. + def destroy! + destroy || raise(ActiveRecord::RecordNotDestroyed) + end + # Returns an instance of the specified +klass+ with the attributes of the # current record. This is mostly useful in relation to single-table # inheritance structures where you want a subclass to appear as the @@ -151,37 +166,6 @@ module ActiveRecord became end - # Updates a single attribute and saves the record. - # This is especially useful for boolean flags on existing records. Also note that - # - # * Validation is skipped. - # * Callbacks are invoked. - # * updated_at/updated_on column is updated if that column is available. - # * Updates all the attributes that are dirty in this object. - # - def update_attribute(name, value) - name = name.to_s - verify_readonly_attribute(name) - send("#{name}=", value) - save(:validate => false) - end - - # Updates a single attribute of an object, without calling save. - # - # * Validation is skipped. - # * 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 - verify_readonly_attribute(name) - raise ActiveRecordError, "can not update on a new record object" unless persisted? - raw_write_attribute(name, value) - self.class.where(self.class.primary_key => id).update_all(name => value) == 1 - end - # Updates the attributes of the model from the passed-in hash and saves the # record, all wrapped in a transaction. If the object is invalid, the saving # will fail and false will be returned. @@ -210,6 +194,40 @@ module ActiveRecord end end + # Updates a single attribute of an object, without calling save. + # + # * Validation is skipped. + # * 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) + update_columns(name => value) + end + + # Updates the attributes from the passed-in hash, without calling save. + # + # * Validation is skipped. + # * 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 at least + # one of the attributes is marked as readonly. + def update_columns(attributes) + raise ActiveRecordError, "can not update on a new record object" unless persisted? + + attributes.each_key do |key| + raise ActiveRecordError, "#{key} is marked as readonly" if self.class.readonly_attributes.include?(key.to_s) + end + + attributes.each do |k,v| + raw_write_attribute(k,v) + end + + self.class.where(self.class.primary_key => id).update_all(attributes) == 1 + end + # Initializes +attribute+ to zero if +nil+ and adds the value passed as +by+ (default is 1). # The increment is performed directly on the underlying attribute, no setter is invoked. # Only makes sense for number-based attributes. Returns +self+. @@ -224,7 +242,7 @@ module ActiveRecord # Saving is not subjected to validation checks. Returns +true+ if the # record could be saved. def increment!(attribute, by = 1) - increment(attribute, by).update_attribute(attribute, self[attribute]) + increment(attribute, by).update_columns(attribute => self[attribute]) end # Initializes +attribute+ to zero if +nil+ and subtracts the value passed as +by+ (default is 1). @@ -241,7 +259,7 @@ module ActiveRecord # Saving is not subjected to validation checks. Returns +true+ if the # record could be saved. def decrement!(attribute, by = 1) - decrement(attribute, by).update_attribute(attribute, self[attribute]) + decrement(attribute, by).update_columns(attribute => self[attribute]) end # Assigns to +attribute+ the boolean opposite of <tt>attribute?</tt>. So @@ -258,7 +276,7 @@ module ActiveRecord # Saving is not subjected to validation checks. Returns +true+ if the # record could be saved. def toggle!(attribute) - toggle(attribute).update_attribute(attribute, self[attribute]) + toggle(attribute).update_columns(attribute => self[attribute]) end # Reloads the attributes of this object from the database. @@ -373,9 +391,5 @@ module ActiveRecord @new_record = false id end - - def verify_readonly_attribute(name) - raise ActiveRecordError, "#{name} is marked as readonly" if self.class.readonly_attributes.include?(name) - end end end diff --git a/activerecord/lib/active_record/query_cache.rb b/activerecord/lib/active_record/query_cache.rb index 9701898415..2bd8ecda20 100644 --- a/activerecord/lib/active_record/query_cache.rb +++ b/activerecord/lib/active_record/query_cache.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/object/blank' module ActiveRecord # = Active Record Query Cache @@ -34,16 +33,22 @@ module ActiveRecord response = @app.call(env) response[2] = Rack::BodyProxy.new(response[2]) do - ActiveRecord::Base.connection_id = connection_id - ActiveRecord::Base.connection.clear_query_cache - ActiveRecord::Base.connection.disable_query_cache! unless enabled + restore_query_cache_settings(connection_id, enabled) end response rescue Exception => e + restore_query_cache_settings(connection_id, enabled) + raise e + end + + private + + def restore_query_cache_settings(connection_id, enabled) + ActiveRecord::Base.connection_id = connection_id ActiveRecord::Base.connection.clear_query_cache ActiveRecord::Base.connection.disable_query_cache! unless enabled - raise e end + end end diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb index 4d8283bcff..13e09eda53 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -1,17 +1,15 @@ -require 'active_support/core_ext/module/delegation' -require 'active_support/deprecation' module ActiveRecord module Querying - delegate :find, :take, :take!, :first, :first!, :last, :last!, :all, :exists?, :any?, :many?, :to => :scoped - delegate :first_or_create, :first_or_create!, :first_or_initialize, :to => :scoped - delegate :find_by, :find_by!, :to => :scoped - delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, :to => :scoped - delegate :find_each, :find_in_batches, :to => :scoped + delegate :find, :take, :take!, :first, :first!, :last, :last!, :exists?, :any?, :many?, :to => :all + delegate :first_or_create, :first_or_create!, :first_or_initialize, :to => :all + delegate :find_by, :find_by!, :to => :all + delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, :to => :all + delegate :find_each, :find_in_batches, :to => :all delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins, :where, :preload, :eager_load, :includes, :from, :lock, :readonly, - :having, :create_with, :uniq, :references, :none, :to => :scoped - delegate :count, :average, :minimum, :maximum, :sum, :calculate, :pluck, :ids, :to => :scoped + :having, :create_with, :uniq, :references, :none, :to => :all + delegate :count, :average, :minimum, :maximum, :sum, :calculate, :pluck, :ids, :to => :all # Executes a custom SQL query against your database and returns all the results. The results will # be returned as an array with columns requested encapsulated as attributes of the model you call @@ -62,8 +60,10 @@ module ActiveRecord # # Product.count_by_sql "SELECT COUNT(*) FROM sales s, customers c WHERE s.customer_id = c.id" def count_by_sql(sql) - sql = sanitize_conditions(sql) - connection.select_value(sql, "#{name} Count").to_i + logging_query_plan do + sql = sanitize_conditions(sql) + connection.select_value(sql, "#{name} Count").to_i + end end end end diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index 1e497b2a79..ecf8547e67 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -29,6 +29,11 @@ module ActiveRecord 'ActiveRecord::RecordNotSaved' => :unprocessable_entity ) + + config.active_record.use_schema_cache_dump = true + + config.eager_load_namespaces << ActiveRecord + rake_tasks do require "active_record/base" load "active_record/railties/databases.rake" @@ -59,11 +64,34 @@ module ActiveRecord ActiveSupport.on_load(:active_record) { self.logger ||= ::Rails.logger } end + initializer "active_record.migration_error" do |app| + if config.active_record.delete(:migration_error) == :page_load + config.app_middleware.insert_after "::ActionDispatch::Callbacks", + "ActiveRecord::Migration::CheckPending" + end + end + + initializer "active_record.check_schema_cache_dump" do |app| + if config.active_record.delete(:use_schema_cache_dump) + config.after_initialize do |app| + ActiveSupport.on_load(:active_record) do + filename = File.join(app.config.paths["db"].first, "schema_cache.dump") + + if File.file?(filename) + cache = Marshal.load File.binread filename + if cache.version == ActiveRecord::Migrator.current_version + ActiveRecord::Model.connection.schema_cache = cache + else + warn "schema_cache.dump is expired. Current version is #{ActiveRecord::Migrator.current_version}, but cache version is #{cache.version}." + end + end + end + end + end + end + initializer "active_record.set_configs" do |app| ActiveSupport.on_load(:active_record) do - if app.config.active_record.delete(:whitelist_attributes) - attr_accessible(nil) - end app.config.active_record.each do |k,v| send "#{k}=", v end @@ -90,18 +118,12 @@ module ActiveRecord end initializer "active_record.set_reloader_hooks" do |app| - hook = lambda do - ActiveRecord::Base.clear_reloadable_connections! - ActiveRecord::Base.clear_cache! - end + hook = app.config.reload_classes_only_on_change ? :to_prepare : :to_cleanup - if app.config.reload_classes_only_on_change - ActiveSupport.on_load(:active_record) do - ActionDispatch::Reloader.to_prepare(&hook) - end - else - ActiveSupport.on_load(:active_record) do - ActionDispatch::Reloader.to_cleanup(&hook) + ActiveSupport.on_load(:active_record) do + ActionDispatch::Reloader.send(hook) do + ActiveRecord::Model.clear_reloadable_connections! + ActiveRecord::Model.clear_cache! end end end @@ -112,24 +134,10 @@ module ActiveRecord config.after_initialize do |app| ActiveSupport.on_load(:active_record) do - ActiveRecord::Base.instantiate_observers + ActiveRecord::Model.instantiate_observers ActionDispatch::Reloader.to_prepare do - ActiveRecord::Base.instantiate_observers - end - end - - ActiveSupport.on_load(:active_record) do - if app.config.use_schema_cache_dump - filename = File.join(app.config.paths["db"].first, "schema_cache.dump") - if File.file?(filename) - cache = Marshal.load File.binread filename - if cache.version == ActiveRecord::Migrator.current_version - ActiveRecord::Base.connection.schema_cache = cache - else - warn "schema_cache.dump is expired. Current version is #{ActiveRecord::Migrator.current_version}, but cache version is #{cache.version}." - end - end + ActiveRecord::Model.instantiate_observers end end diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index d8d4834d22..4e5ec4f739 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -1,8 +1,7 @@ -require 'active_support/core_ext/object/inclusion' require 'active_record' db_namespace = namespace :db do - task :load_config => :rails_env do + task :load_config do ActiveRecord::Base.configurations = Rails.application.config.database_configuration ActiveRecord::Migrator.migrations_paths = Rails.application.paths['db/migrate'].to_a @@ -14,139 +13,28 @@ db_namespace = namespace :db do end namespace :create do - # desc 'Create all the local databases defined in config/database.yml' task :all => :load_config do - ActiveRecord::Base.configurations.each_value do |config| - # Skip entries that don't have a database key, such as the first entry here: - # - # defaults: &defaults - # adapter: mysql - # username: root - # password: - # host: localhost - # - # development: - # database: blog_development - # *defaults - next unless config['database'] - # Only connect to local databases - local_database?(config) { create_database(config) } - end + ActiveRecord::Tasks::DatabaseTasks.create_all end end desc 'Create the database from config/database.yml for the current Rails.env (use db:create:all to create all dbs in the config)' - task :create => :load_config do - configs_for_environment.each { |config| create_database(config) } - ActiveRecord::Base.establish_connection(configs_for_environment.first) - end - - def mysql_creation_options(config) - @charset = ENV['CHARSET'] || 'utf8' - @collation = ENV['COLLATION'] || 'utf8_unicode_ci' - {:charset => (config['charset'] || @charset), :collation => (config['collation'] || @collation)} - end - - def create_database(config) - begin - if config['adapter'] =~ /sqlite/ - if File.exist?(config['database']) - $stderr.puts "#{config['database']} already exists" - else - begin - # Create the SQLite database - ActiveRecord::Base.establish_connection(config) - ActiveRecord::Base.connection - rescue Exception => e - $stderr.puts e, *(e.backtrace) - $stderr.puts "Couldn't create database for #{config.inspect}" - end - end - return # Skip the else clause of begin/rescue - else - ActiveRecord::Base.establish_connection(config) - ActiveRecord::Base.connection - end - rescue - case config['adapter'] - when /mysql/ - if config['adapter'] =~ /jdbc/ - #FIXME After Jdbcmysql gives this class - require 'active_record/railties/jdbcmysql_error' - error_class = ArJdbcMySQL::Error - else - error_class = config['adapter'] =~ /mysql2/ ? Mysql2::Error : Mysql::Error - end - access_denied_error = 1045 - begin - ActiveRecord::Base.establish_connection(config.merge('database' => nil)) - ActiveRecord::Base.connection.create_database(config['database'], mysql_creation_options(config)) - ActiveRecord::Base.establish_connection(config) - rescue error_class => sqlerr - if sqlerr.errno == access_denied_error - print "#{sqlerr.error}. \nPlease provide the root password for your mysql installation\n>" - root_password = $stdin.gets.strip - grant_statement = "GRANT ALL PRIVILEGES ON #{config['database']}.* " \ - "TO '#{config['username']}'@'localhost' " \ - "IDENTIFIED BY '#{config['password']}' WITH GRANT OPTION;" - ActiveRecord::Base.establish_connection(config.merge( - 'database' => nil, 'username' => 'root', 'password' => root_password)) - ActiveRecord::Base.connection.create_database(config['database'], mysql_creation_options(config)) - ActiveRecord::Base.connection.execute grant_statement - ActiveRecord::Base.establish_connection(config) - else - $stderr.puts sqlerr.error - $stderr.puts "Couldn't create database for #{config.inspect}, charset: #{config['charset'] || @charset}, collation: #{config['collation'] || @collation}" - $stderr.puts "(if you set the charset manually, make sure you have a matching collation)" if config['charset'] - end - end - when /postgresql/ - @encoding = config['encoding'] || ENV['CHARSET'] || 'utf8' - begin - ActiveRecord::Base.establish_connection(config.merge('database' => 'postgres', 'schema_search_path' => 'public')) - ActiveRecord::Base.connection.create_database(config['database'], config.merge('encoding' => @encoding)) - ActiveRecord::Base.establish_connection(config) - rescue Exception => e - $stderr.puts e, *(e.backtrace) - $stderr.puts "Couldn't create database for #{config.inspect}" - end - end - else - $stderr.puts "#{config['database']} already exists" - end + task :create => [:load_config] do + ActiveRecord::Tasks::DatabaseTasks.create_current end namespace :drop do - # desc 'Drops all the local databases defined in config/database.yml' task :all => :load_config do - ActiveRecord::Base.configurations.each_value do |config| - # Skip entries that don't have a database key - next unless config['database'] - begin - # Only connect to local databases - local_database?(config) { drop_database(config) } - rescue Exception => e - $stderr.puts "Couldn't drop #{config['database']} : #{e.inspect}" - end - end + ActiveRecord::Tasks::DatabaseTasks.drop_all end end desc 'Drops the database for the current Rails.env (use db:drop:all to drop all databases)' - task :drop => :load_config do - configs_for_environment.each { |config| drop_database_and_rescue(config) } + task :drop => [:load_config] do + ActiveRecord::Tasks::DatabaseTasks.drop_current end - def local_database?(config, &block) - if config['host'].in?(['127.0.0.1', 'localhost']) || config['host'].blank? - yield - else - $stderr.puts "This task only modifies local databases. #{config['database']} is on a remote host." - end - end - - - desc "Migrate the database (options: VERSION=x, VERBOSE=false)." + desc "Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)." task :migrate => [:environment, :load_config] do ActiveRecord::Migration.verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] == "true" : true ActiveRecord::Migrator.migrate(ActiveRecord::Migrator.migrations_paths, ENV["VERSION"] ? ENV["VERSION"].to_i : nil) do |migration| @@ -200,7 +88,7 @@ db_namespace = namespace :db do desc 'Display status of migrations' task :status => [:environment, :load_config] do - config = ActiveRecord::Base.configurations[Rails.env || 'development'] + config = ActiveRecord::Base.configurations[Rails.env] ActiveRecord::Base.establish_connection(config) unless ActiveRecord::Base.connection.table_exists?(ActiveRecord::Migrator.schema_migrations_table_name) puts 'Schema migrations table does not exist yet.' @@ -247,48 +135,32 @@ db_namespace = namespace :db do end # desc 'Drops and recreates the database from db/schema.rb for the current environment and loads the seeds.' - task :reset => :environment do + task :reset => [:environment, :load_config] do db_namespace["drop"].invoke db_namespace["setup"].invoke end # desc "Retrieves the charset for the current environment's database" - task :charset => :environment do - config = ActiveRecord::Base.configurations[Rails.env || 'development'] - case config['adapter'] - when /mysql/ - ActiveRecord::Base.establish_connection(config) - puts ActiveRecord::Base.connection.charset - when /postgresql/ - ActiveRecord::Base.establish_connection(config) - puts ActiveRecord::Base.connection.encoding - when /sqlite/ - ActiveRecord::Base.establish_connection(config) - puts ActiveRecord::Base.connection.encoding - else - $stderr.puts 'sorry, your database adapter is not supported yet, feel free to submit a patch' - end + task :charset => [:environment, :load_config] do + puts ActiveRecord::Tasks::DatabaseTasks.charset_current end # desc "Retrieves the collation for the current environment's database" - task :collation => :environment do - config = ActiveRecord::Base.configurations[Rails.env || 'development'] - case config['adapter'] - when /mysql/ - ActiveRecord::Base.establish_connection(config) - puts ActiveRecord::Base.connection.collation - else - $stderr.puts 'sorry, your database adapter is not supported yet, feel free to submit a patch' + task :collation => [:environment, :load_config] do + begin + puts ActiveRecord::Tasks::DatabaseTasks.collation_current + rescue NoMethodError + $stderr.puts 'Sorry, your database adapter is not supported yet, feel free to submit a patch' end end desc 'Retrieves the current schema version number' - task :version => :environment do + task :version => [:environment, :load_config] do puts "Current version: #{ActiveRecord::Migrator.current_version}" end # desc "Raises an error if there are pending migrations" - task :abort_if_pending_migrations => :environment do + task :abort_if_pending_migrations => [:environment, :load_config] do pending_migrations = ActiveRecord::Migrator.new(:up, ActiveRecord::Migrator.migrations_paths).pending_migrations if pending_migrations.any? @@ -311,20 +183,20 @@ db_namespace = namespace :db do namespace :fixtures do desc "Load fixtures into the current environment's database. Load specific fixtures using FIXTURES=x,y. Load from subdirectory in test/fixtures using FIXTURES_DIR=z. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures." - task :load => :environment do + task :load => [:environment, :load_config] do require 'active_record/fixtures' ActiveRecord::Base.establish_connection(Rails.env) base_dir = File.join [Rails.root, ENV['FIXTURES_PATH'] || %w{test fixtures}].flatten fixtures_dir = File.join [base_dir, ENV['FIXTURES_DIR']].compact - (ENV['FIXTURES'] ? ENV['FIXTURES'].split(/,/) : Dir["#{fixtures_dir}/**/*.{yml,csv}"].map {|f| f[(fixtures_dir.size + 1)..-5] }).each do |fixture_file| + (ENV['FIXTURES'] ? ENV['FIXTURES'].split(/,/) : Dir["#{fixtures_dir}/**/*.yml"].map {|f| f[(fixtures_dir.size + 1)..-5] }).each do |fixture_file| ActiveRecord::Fixtures.create_fixtures(fixtures_dir, fixture_file) end end # desc "Search for a fixture given a LABEL or ID. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures." - task :identify => :environment do + task :identify => [:environment, :load_config] do require 'active_record/fixtures' label, id = ENV['LABEL'], ENV['ID'] @@ -360,7 +232,7 @@ db_namespace = namespace :db do end desc 'Load a schema.rb file into the database' - task :load => :environment do + task :load => [:environment, :load_config] do file = ENV['SCHEMA'] || "#{Rails.root}/db/schema.rb" if File.exists?(file) load(file) @@ -369,13 +241,13 @@ db_namespace = namespace :db do end end - task :load_if_ruby => 'db:create' do + task :load_if_ruby => [:environment, 'db:create'] do db_namespace["schema:load"].invoke if ActiveRecord::Base.schema_format == :ruby end namespace :cache do desc 'Create a db/schema_cache.dump file.' - task :dump => :environment do + task :dump => [:environment, :load_config] do con = ActiveRecord::Base.connection filename = File.join(Rails.application.config.paths["db"].first, "schema_cache.dump") @@ -385,7 +257,7 @@ db_namespace = namespace :db do end desc 'Clear a db/schema_cache.dump file.' - task :clear => :environment do + task :clear => [:environment, :load_config] do filename = File.join(Rails.application.config.paths["db"].first, "schema_cache.dump") FileUtils.rm(filename) if File.exists?(filename) end @@ -394,26 +266,25 @@ db_namespace = namespace :db do end namespace :structure do + def set_firebird_env(config) + ENV['ISC_USER'] = config['username'].to_s if config['username'] + ENV['ISC_PASSWORD'] = config['password'].to_s if config['password'] + end + + def firebird_db_string(config) + FireRuby::Database.db_string_for(config.symbolize_keys) + end + desc 'Dump the database structure to db/structure.sql. Specify another file with DB_STRUCTURE=db/my_structure.sql' - task :dump => :environment do + task :dump => [:environment, :load_config] do abcs = ActiveRecord::Base.configurations filename = ENV['DB_STRUCTURE'] || File.join(Rails.root, "db", "structure.sql") case abcs[Rails.env]['adapter'] - when /mysql/, 'oci', 'oracle' + when /mysql/, /postgresql/, /sqlite/ + ActiveRecord::Tasks::DatabaseTasks.structure_dump(abcs[Rails.env], filename) + when 'oci', 'oracle' ActiveRecord::Base.establish_connection(abcs[Rails.env]) File.open(filename, "w:utf-8") { |f| f << ActiveRecord::Base.connection.structure_dump } - when /postgresql/ - set_psql_env(abcs[Rails.env]) - search_path = abcs[Rails.env]['schema_search_path'] - unless search_path.blank? - search_path = search_path.split(",").map{|search_path_part| "--schema=#{Shellwords.escape(search_path_part.strip)}" }.join(" ") - end - `pg_dump -i -s -x -O -f #{Shellwords.escape(filename)} #{search_path} #{Shellwords.escape(abcs[Rails.env]['database'])}` - raise 'Error dumping database' if $?.exitstatus == 1 - File.open(filename, "a") { |f| f << "SET search_path TO #{ActiveRecord::Base.connection.schema_search_path};\n\n" } - when /sqlite/ - dbfile = abcs[Rails.env]['database'] - `sqlite3 #{dbfile} .schema > #{filename}` when 'sqlserver' `smoscript -s #{abcs[Rails.env]['host']} -d #{abcs[Rails.env]['database']} -u #{abcs[Rails.env]['username']} -p #{abcs[Rails.env]['password']} -f #{filename} -A -U` when "firebird" @@ -437,18 +308,8 @@ db_namespace = namespace :db do abcs = ActiveRecord::Base.configurations filename = ENV['DB_STRUCTURE'] || File.join(Rails.root, "db", "structure.sql") case abcs[env]['adapter'] - when /mysql/ - ActiveRecord::Base.establish_connection(abcs[env]) - ActiveRecord::Base.connection.execute('SET foreign_key_checks = 0') - IO.read(filename).split("\n\n").each do |table| - ActiveRecord::Base.connection.execute(table) - end - when /postgresql/ - set_psql_env(abcs[env]) - `psql -f "#{filename}" #{abcs[env]['database']}` - when /sqlite/ - dbfile = abcs[env]['database'] - `sqlite3 #{dbfile} < "#{filename}"` + when /mysql/, /postgresql/, /sqlite/ + ActiveRecord::Tasks::DatabaseTasks.structure_load(abcs[env], filename) when 'sqlserver' `sqlcmd -S #{abcs[env]['host']} -d #{abcs[env]['database']} -U #{abcs[env]['username']} -P #{abcs[env]['password']} -i #{filename}` when 'oci', 'oracle' @@ -465,7 +326,7 @@ db_namespace = namespace :db do end end - task :load_if_sql => 'db:create' do + task :load_if_sql => [:environment, 'db:create'] do db_namespace["structure:load"].invoke if ActiveRecord::Base.schema_format == :sql end end @@ -482,6 +343,13 @@ db_namespace = namespace :db do end end + # desc "Recreate the test database from an existent schema.rb file" + task :load_schema => 'db:test:purge' do + ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['test']) + ActiveRecord::Schema.verbose = false + db_namespace["schema:load"].invoke + end + # desc "Recreate the test database from an existent structure.sql file" task :load_structure => 'db:test:purge' do begin @@ -492,33 +360,28 @@ db_namespace = namespace :db do end end - # desc "Recreate the test database from an existent schema.rb file" - task :load_schema => 'db:test:purge' do - ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['test']) - ActiveRecord::Schema.verbose = false - db_namespace["schema:load"].invoke + # desc "Recreate the test database from a fresh schema" + task :clone do + case ActiveRecord::Base.schema_format + when :ruby + db_namespace["test:clone_schema"].invoke + when :sql + db_namespace["test:clone_structure"].invoke + end end # desc "Recreate the test database from a fresh schema.rb file" - task :clone => %w(db:schema:dump db:test:load_schema) + task :clone_schema => ["db:schema:dump", "db:test:load_schema"] # desc "Recreate the test database from a fresh structure.sql file" task :clone_structure => [ "db:structure:dump", "db:test:load_structure" ] # desc "Empty the test database" - task :purge => :environment do + task :purge => [:environment, :load_config] do abcs = ActiveRecord::Base.configurations case abcs['test']['adapter'] - when /mysql/ - ActiveRecord::Base.establish_connection(:test) - ActiveRecord::Base.connection.recreate_database(abcs['test']['database'], mysql_creation_options(abcs['test'])) - when /postgresql/ - ActiveRecord::Base.clear_active_connections! - drop_database(abcs['test']) - create_database(abcs['test']) - when /sqlite/ - dbfile = abcs['test']['database'] - File.delete(dbfile) if File.exist?(dbfile) + when /mysql/, /postgresql/, /sqlite/ + ActiveRecord::Tasks::DatabaseTasks.purge abcs['test'] when 'sqlserver' test = abcs.deep_dup['test'] test_database = test['database'] @@ -541,14 +404,14 @@ db_namespace = namespace :db do # desc 'Check for pending migrations and load the test schema' task :prepare => 'db:abort_if_pending_migrations' do unless ActiveRecord::Base.configurations.blank? - db_namespace[{ :sql => 'test:clone_structure', :ruby => 'test:load' }[ActiveRecord::Base.schema_format]].invoke + db_namespace['test:load'].invoke end end end namespace :sessions do # desc "Creates a sessions migration for use with ActiveRecord::SessionStore" - task :create => :environment do + task :create => [:environment, :load_config] do raise 'Task unavailable to this database (no migration support)' unless ActiveRecord::Base.connection.supports_migrations? Rails.application.load_generators require 'rails/generators/rails/session_migration/session_migration_generator' @@ -556,8 +419,8 @@ db_namespace = namespace :db do end # desc "Clear the sessions table" - task :clear => :environment do - ActiveRecord::Base.connection.execute "DELETE FROM #{session_table_name}" + task :clear => [:environment, :load_config] do + ActiveRecord::Base.connection.execute "DELETE FROM #{ActiveRecord::SessionStore::Session.table_name}" end end end @@ -568,7 +431,7 @@ namespace :railties do task :migrations => :'db:load_config' do to_load = ENV['FROM'].blank? ? :all : ENV['FROM'].split(",").map {|n| n.strip } railties = {} - Rails.application.railties.all do |railtie| + Rails.application.railties.each do |railtie| next unless to_load == :all || to_load.include?(railtie.railtie_name) if railtie.respond_to?(:paths) && (path = railtie.paths['db/migrate'].first) @@ -584,7 +447,7 @@ namespace :railties do puts "Copied migration #{migration.basename} from #{name}" end - ActiveRecord::Migration.copy( ActiveRecord::Migrator.migrations_paths.first, railties, + ActiveRecord::Migration.copy(ActiveRecord::Migrator.migrations_paths.first, railties, :on_skip => on_skip, :on_copy => on_copy) end end @@ -592,53 +455,3 @@ end task 'test:prepare' => 'db:test:prepare' -def drop_database(config) - case config['adapter'] - when /mysql/ - ActiveRecord::Base.establish_connection(config) - ActiveRecord::Base.connection.drop_database config['database'] - when /sqlite/ - require 'pathname' - path = Pathname.new(config['database']) - file = path.absolute? ? path.to_s : File.join(Rails.root, path) - - FileUtils.rm(file) - when /postgresql/ - ActiveRecord::Base.establish_connection(config.merge('database' => 'postgres', 'schema_search_path' => 'public')) - ActiveRecord::Base.connection.drop_database config['database'] - end -end - -def drop_database_and_rescue(config) - begin - drop_database(config) - rescue Exception => e - $stderr.puts "Couldn't drop #{config['database']} : #{e.inspect}" - end -end - -def configs_for_environment - environments = [Rails.env] - environments << 'test' if Rails.env.development? - ActiveRecord::Base.configurations.values_at(*environments).compact.reject { |config| config['database'].blank? } -end - -def session_table_name - ActiveRecord::SessionStore::Session.table_name -end - -def set_firebird_env(config) - ENV['ISC_USER'] = config['username'].to_s if config['username'] - ENV['ISC_PASSWORD'] = config['password'].to_s if config['password'] -end - -def firebird_db_string(config) - FireRuby::Database.db_string_for(config.symbolize_keys) -end - -def set_psql_env(config) - ENV['PGHOST'] = config['host'] if config['host'] - ENV['PGPORT'] = config['port'].to_s if config['port'] - ENV['PGPASSWORD'] = config['password'].to_s if config['password'] - ENV['PGUSER'] = config['username'].to_s if config['username'] -end diff --git a/activerecord/lib/active_record/readonly_attributes.rb b/activerecord/lib/active_record/readonly_attributes.rb index 836b15e2ce..b3c20c4aff 100644 --- a/activerecord/lib/active_record/readonly_attributes.rb +++ b/activerecord/lib/active_record/readonly_attributes.rb @@ -1,12 +1,10 @@ -require 'active_support/concern' -require 'active_support/core_ext/class/attribute' module ActiveRecord module ReadonlyAttributes extend ActiveSupport::Concern included do - config_attribute :_attr_readonly + class_attribute :_attr_readonly, instance_accessor: false self._attr_readonly = [] end @@ -22,5 +20,10 @@ module ActiveRecord self._attr_readonly end end + + def _attr_readonly + ActiveSupport::Deprecation.warn("Instance level _attr_readonly method is deprecated, please use class level method.") + defined?(@_attr_readonly) ? @_attr_readonly : self.class._attr_readonly + end end end diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index c380b5c029..cf949a893f 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -1,5 +1,3 @@ -require 'active_support/core_ext/class/attribute' -require 'active_support/core_ext/object/inclusion' module ActiveRecord # = Active Record Reflection @@ -7,8 +5,7 @@ module ActiveRecord extend ActiveSupport::Concern included do - extend ActiveModel::Configuration - config_attribute :reflections + class_attribute :reflections self.reflections = {} end @@ -21,13 +18,13 @@ module ActiveRecord # MacroReflection class has info for AggregateReflection and AssociationReflection # classes. module ClassMethods - def create_reflection(macro, name, options, active_record) + def create_reflection(macro, name, scope, options, active_record) case macro when :has_many, :belongs_to, :has_one, :has_and_belongs_to_many klass = options[:through] ? ThroughReflection : AssociationReflection - reflection = klass.new(macro, name, options, active_record) + reflection = klass.new(macro, name, scope, options, active_record) when :composed_of - reflection = AggregateReflection.new(macro, name, options, active_record) + reflection = AggregateReflection.new(macro, name, scope, options, active_record) end self.reflections = self.reflections.merge(name => reflection) @@ -94,6 +91,8 @@ module ActiveRecord # <tt>has_many :clients</tt> returns <tt>:has_many</tt> attr_reader :macro + attr_reader :scope + # Returns the hash of options used for the macro. # # <tt>composed_of :balance, :class_name => 'Money'</tt> returns <tt>{ :class_name => "Money" }</tt> @@ -104,9 +103,10 @@ module ActiveRecord attr_reader :plural_name # :nodoc: - def initialize(macro, name, options, active_record) + def initialize(macro, name, scope, options, active_record) @macro = macro @name = name + @scope = scope @options = options @active_record = active_record @plural_name = active_record.pluralize_table_names ? @@ -139,10 +139,6 @@ module ActiveRecord active_record == other_aggregation.active_record end - def sanitized_conditions #:nodoc: - @sanitized_conditions ||= klass.send(:sanitize_sql, options[:conditions]) if options[:conditions] - end - private def derive_class_name name.to_s.camelize @@ -178,7 +174,7 @@ module ActiveRecord @klass ||= active_record.send(:compute_type, class_name) end - def initialize(macro, name, options, active_record) + def initialize(*args) super @collection = [:has_many, :has_and_belongs_to_many].include?(macro) end @@ -197,6 +193,10 @@ module ActiveRecord @quoted_table_name ||= klass.quoted_table_name end + def join_table + @join_table ||= options[:join_table] || derive_join_table + end + def foreign_key @foreign_key ||= options[:foreign_key] || derive_foreign_key end @@ -244,6 +244,10 @@ module ActiveRecord def check_validity! check_validity_of_inverse! + + if has_and_belongs_to_many? && association_foreign_key == foreign_key + raise HasAndBelongsToManyAssociationForeignKeyNeeded.new(self) + end end def check_validity_of_inverse! @@ -272,11 +276,10 @@ module ActiveRecord false end - # An array of arrays of conditions. Each item in the outside array corresponds to a reflection - # in the #chain. The inside arrays are simply conditions (and each condition may itself be - # a hash, array, arel predicate, etc...) - def conditions - [[options[:conditions]].compact] + # An array of arrays of scopes. Each item in the outside array corresponds to a reflection + # in the #chain. + def scope_chain + scope ? [[scope]] : [[]] end alias :source_macro :macro @@ -326,6 +329,10 @@ module ActiveRecord macro == :belongs_to end + def has_and_belongs_to_many? + macro == :has_and_belongs_to_many + end + def association_class case macro when :belongs_to @@ -368,6 +375,10 @@ module ActiveRecord end end + def derive_join_table + [active_record.table_name, klass.table_name].sort.join("\0").gsub(/^(.*_)(.+)\0\1(.+)/, '\1\2_\3').gsub("\0", "_") + end + def primary_key(klass) klass.primary_key || raise(UnknownPrimaryKey.new(klass)) end @@ -436,28 +447,25 @@ module ActiveRecord # has_many :tags # end # - # There may be conditions on Person.comment_tags, Article.comment_tags and/or Comment.tags, + # There may be scopes on Person.comment_tags, Article.comment_tags and/or Comment.tags, # but only Comment.tags will be represented in the #chain. So this method creates an array - # of conditions corresponding to the chain. Each item in the #conditions array corresponds - # to an item in the #chain, and is itself an array of conditions from an arbitrary number - # of relevant reflections, plus any :source_type or polymorphic :as constraints. - def conditions - @conditions ||= begin - conditions = source_reflection.conditions.map { |c| c.dup } + # of scopes corresponding to the chain. + def scope_chain + @scope_chain ||= begin + scope_chain = source_reflection.scope_chain.map(&:dup) - # Add to it the conditions from this reflection if necessary. - conditions.first << options[:conditions] if options[:conditions] + # Add to it the scope from this reflection (if any) + scope_chain.first << scope if scope - through_conditions = through_reflection.conditions + through_scope_chain = through_reflection.scope_chain if options[:source_type] - through_conditions.first << { foreign_type => options[:source_type] } + through_scope_chain.first << + through_reflection.klass.where(foreign_type => options[:source_type]) end # Recursively fill out the rest of the array from the through reflection - conditions += through_conditions - - conditions + scope_chain + through_scope_chain end end diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index eea164e38d..2d0457636e 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -1,6 +1,4 @@ # -*- coding: utf-8 -*- -require 'active_support/core_ext/object/blank' -require 'active_support/deprecation' module ActiveRecord # = Active Record Relation @@ -76,6 +74,18 @@ module ActiveRecord binds) end + # Initializes new record from relation while maintaining the current + # scope. + # + # Expects arguments in the same format as +Base.new+. + # + # users = User.where(name: 'DHH') + # user = users.new # => #<User id: nil, name: "DHH", created_at: nil, updated_at: nil> + # + # You can also pass a block to new with the new record as argument: + # + # user = users.new { |user| user.name = 'Oscar' } + # user.name # => Oscar def new(*args, &block) scoping { @klass.new(*args, &block) } end @@ -88,17 +98,38 @@ module ActiveRecord alias build new + # Tries to create a new record with the same scoped attributes + # defined in the relation. Returns the initialized object if validation fails. + # + # Expects arguments in the same format as +Base.create+. + # + # ==== Examples + # users = User.where(name: 'Oscar') + # users.create # #<User id: 3, name: "oscar", ...> + # + # users.create(name: 'fxn') + # users.create # #<User id: 4, name: "fxn", ...> + # + # users.create { |user| user.name = 'tenderlove' } + # # #<User id: 5, name: "tenderlove", ...> + # + # users.create(name: nil) # validation on name + # # #<User id: nil, name: nil, ...> def create(*args, &block) scoping { @klass.create(*args, &block) } end + # Similar to #create, but calls +create!+ on the base class. Raises + # an exception if a validation error occurs. + # + # Expects arguments in the same format as <tt>Base.create!</tt>. def create!(*args, &block) scoping { @klass.create!(*args, &block) } end # Tries to load the first record; if it fails, then <tt>create</tt> is called with the same arguments as this method. # - # Expects arguments in the same format as <tt>Base.create</tt>. + # Expects arguments in the same format as +Base.create+. # # ==== Examples # # Find the first user named Penélope or create a new one. @@ -146,52 +177,17 @@ module ActiveRecord # are needed by the next ones when eager loading is going on. # # Please see further details in the - # {Active Record Query Interface guide}[http://edgeguides.rubyonrails.org/active_record_querying.html#running-explain]. + # {Active Record Query Interface guide}[http://guides.rubyonrails.org/active_record_querying.html#running-explain]. def explain _, queries = collecting_queries_for_explain { exec_queries } exec_explain(queries) end + # Converts relation objects to Array. def to_a - # We monitor here the entire execution rather than individual SELECTs - # because from the point of view of the user fetching the records of a - # relation is a single unit of work. You want to know if this call takes - # too long, not if the individual queries take too long. - # - # It could be the case that none of the queries involved surpass the - # threshold, and at the same time the sum of them all does. The user - # should get a query plan logged in that case. - logging_query_plan do - exec_queries - end - end - - def exec_queries - return @records if loaded? - - default_scoped = with_default_scope - - if default_scoped.equal?(self) - @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel, bind_values) - - 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 - else - @records = default_scoped.to_a - end - - @loaded = true + load @records end - private :exec_queries def as_json(options = nil) #:nodoc: to_a.as_json(options) @@ -210,6 +206,7 @@ module ActiveRecord c.respond_to?(:zero?) ? c.zero? : c.empty? end + # Returns true if there are any records. def any? if block_given? to_a.any? { |*block_args| yield(*block_args) } @@ -218,6 +215,7 @@ module ActiveRecord end end + # Returns true if there is more than one record. def many? if block_given? to_a.many? { |*block_args| yield(*block_args) } @@ -228,8 +226,6 @@ module ActiveRecord # Scope all queries to the current scope. # - # ==== Example - # # Comment.where(:post_id => 1).scoping do # Comment.first # SELECT * FROM comments WHERE post_id = 1 # end @@ -251,21 +247,20 @@ module ActiveRecord # ==== Parameters # # * +updates+ - A string, array, or hash representing the SET part of an SQL statement. - # * +conditions+ - A string, array, or hash representing the WHERE part of an SQL statement. - # See conditions in the intro. - # * +options+ - Additional options are <tt>:limit</tt> and <tt>:order</tt>, see the examples for usage. # # ==== Examples # # # Update all customers with the given attributes - # Customer.update_all :wants_email => true + # Customer.update_all wants_email: true # # # Update all books with 'Rails' in their title - # Book.where('title LIKE ?', '%Rails%').update_all(:author => 'David') + # Book.where('title LIKE ?', '%Rails%').update_all(author: 'David') # # # Update all books that match conditions, but limit it to 5 ordered by date # Book.where('title LIKE ?', '%Rails%').order(:created_at).limit(5).update_all(:author => 'David') def update_all(updates) + raise ArgumentError, "Empty list of attributes to change" if updates.blank? + stmt = Arel::UpdateManager.new(arel.engine) stmt.set Arel.sql(@klass.send(:sanitize_sql_for_assignment, updates)) @@ -294,14 +289,14 @@ module ActiveRecord # ==== Examples # # # Updates one record - # Person.update(15, :user_name => 'Samuel', :group => 'expert') + # Person.update(15, user_name: 'Samuel', group: 'expert') # # # Updates multiple records # people = { 1 => { "first_name" => "David" }, 2 => { "first_name" => "Jeremy" } } # Person.update(people.keys, people.values) def update(id, attributes) if id.is_a?(Array) - id.each.with_index.map {|one_id, idx| update(one_id, attributes[idx])} + id.map.with_index { |one_id, idx| update(one_id, attributes[idx]) } else object = find(id) object.update_attributes(attributes) @@ -334,7 +329,7 @@ module ActiveRecord # ==== Examples # # Person.destroy_all("last_login < '2004-04-04'") - # Person.destroy_all(:status => "inactive") + # Person.destroy_all(status: "inactive") # Person.where(:age => 0..18).destroy_all def destroy_all(conditions = nil) if conditions @@ -436,10 +431,32 @@ module ActiveRecord where(primary_key => id_or_array).delete_all end + # Causes the records to be loaded from the database if they have not + # been loaded already. You can use this if for some reason you need + # to explicitly load some records before actually using them. The + # return value is the relation itself, not the records. + # + # Post.where(published: true).load # => #<ActiveRecord::Relation> + def load + unless loaded? + # We monitor here the entire execution rather than individual SELECTs + # because from the point of view of the user fetching the records of a + # relation is a single unit of work. You want to know if this call takes + # too long, not if the individual queries take too long. + # + # It could be the case that none of the queries involved surpass the + # threshold, and at the same time the sum of them all does. The user + # should get a query plan logged in that case. + logging_query_plan { exec_queries } + end + + self + end + + # Forces reloading of relation. def reload reset - to_a # force reload - self + load end def reset @@ -449,10 +466,18 @@ module ActiveRecord self end + # Returns sql statement for the relation. + # + # Users.where(name: 'Oscar').to_sql + # # => SELECT "users".* FROM "users" WHERE "users"."name" = 'Oscar' def to_sql @to_sql ||= klass.connection.to_sql(arel, bind_values.dup) end + # Returns a hash of where conditions + # + # Users.where(name: 'Oscar').where_values_hash + # # => {:name=>"oscar"} def where_values_hash equalities = with_default_scope.where_values.grep(Arel::Nodes::Equality).find_all { |node| node.left.relation.name == table_name @@ -470,6 +495,7 @@ module ActiveRecord @scope_for_create ||= where_values_hash.merge(create_with_value) end + # Returns true if relation needs eager loading. def eager_loading? @should_eager_load ||= eager_load_values.any? || @@ -484,6 +510,7 @@ module ActiveRecord includes_values & joins_values end + # Compares two relations for equality. def ==(other) case other when Relation @@ -493,10 +520,6 @@ module ActiveRecord end end - def inspect - to_a.inspect - end - def pretty_print(q) q.pp(self.to_a) end @@ -511,6 +534,7 @@ module ActiveRecord end end + # Returns true if relation is blank. def blank? to_a.blank? end @@ -519,8 +543,39 @@ module ActiveRecord @values.dup end + def inspect + entries = to_a.take([limit_value, 11].compact.min).map!(&:inspect) + entries[10] = '...' if entries.size == 11 + + "#<#{self.class.name} [#{entries.join(', ')}]>" + end + private + def exec_queries + default_scoped = with_default_scope + + if default_scoped.equal?(self) + @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel, bind_values) + + 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 + else + @records = default_scoped.to_a + end + + @loaded = true + @records + end + def references_eager_loaded_tables? joined_tables = arel.join_sources.map do |join| if join.is_a?(Arel::Nodes::StringJoin) diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb index fb4388d4b2..4d14506965 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/object/blank' module ActiveRecord module Batches @@ -9,8 +8,8 @@ module ActiveRecord # In that case, batch processing methods allow you to work # with the records in batches, thereby greatly reducing memory consumption. # - # The <tt>find_each</tt> method uses <tt>find_in_batches</tt> with a batch size of 1000 (or as - # specified by the <tt>:batch_size</tt> option). + # The #find_each method uses #find_in_batches with a batch size of 1000 (or as + # specified by the +:batch_size+ option). # # Person.all.find_each do |person| # person.do_awesome_stuff @@ -20,7 +19,7 @@ module ActiveRecord # person.party_all_night! # end # - # You can also pass the <tt>:start</tt> option to specify + # You can also pass the +:start+ option to specify # an offset to control the starting point. def find_each(options = {}) find_in_batches(options) do |records| @@ -29,14 +28,14 @@ module ActiveRecord end # Yields each batch of records that was found by the find +options+ as - # an array. The size of each batch is set by the <tt>:batch_size</tt> + # an array. The size of each batch is set by the +:batch_size+ # option; the default is 1000. # # You can control the starting point for the batch processing by - # supplying the <tt>:start</tt> option. This is especially useful if you + # supplying the +:start+ option. This is especially useful if you # want multiple workers dealing with the same processing queue. You can # make worker 1 handle all the records between id 0 and 10,000 and - # worker 2 handle from 10,000 and beyond (by setting the <tt>:start</tt> + # worker 2 handle from 10,000 and beyond (by setting the +:start+ # option on that worker). # # It's not possible to set the order. That is automatically set to @@ -67,7 +66,7 @@ module ActiveRecord batch_size = options.delete(:batch_size) || 1000 relation = relation.reorder(batch_order).limit(batch_size) - records = relation.where(table[primary_key].gteq(start)).all + records = relation.where(table[primary_key].gteq(start)).to_a while records.any? records_size = records.size diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 54c93332bb..d93e7c8997 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/object/blank' require 'active_support/core_ext/object/try' module ActiveRecord @@ -107,7 +106,7 @@ module ActiveRecord relation = with_default_scope if relation.equal?(self) - if eager_loading? || (includes_values.present? && references_eager_loaded_tables?) + if has_include?(column_name) construct_relation_for_association_calculations.calculate(operation, column_name, options) else perform_calculation(operation, column_name, options) @@ -138,6 +137,10 @@ module ActiveRecord # # SELECT people.id FROM people # # => [1, 2, 3] # + # Person.pluck(:id, :name) + # # SELECT people.id, people.name FROM people + # # => [[1, 'David'], [2, 'Jeremy'], [3, 'Jose']] + # # Person.uniq.pluck(:role) # # SELECT DISTINCT role FROM people # # => ['admin', 'member', 'guest'] @@ -150,26 +153,35 @@ module ActiveRecord # # SELECT DATEDIFF(updated_at, created_at) FROM people # # => ['0', '27761', '173'] # - def pluck(column_name) - if column_name.is_a?(Symbol) && column_names.include?(column_name.to_s) - column_name = "#{table_name}.#{column_name}" + def pluck(*column_names) + column_names.map! do |column_name| + if column_name.is_a?(Symbol) && self.column_names.include?(column_name.to_s) + "#{table_name}.#{column_name}" + else + column_name + end end - result = klass.connection.select_all(select(column_name).arel, nil, bind_values) - - key = result.columns.first - column = klass.column_types.fetch(key) { - result.column_types.fetch(key) { - Class.new { def type_cast(v); v; end }.new - } - } - - result.map do |attributes| - raise ArgumentError, "Pluck expects to select just one attribute: #{attributes.inspect}" unless attributes.one? + if has_include?(column_names.first) + construct_relation_for_association_calculations.pluck(*column_names) + else + result = klass.connection.select_all(select(column_names).arel, nil, bind_values) + columns = result.columns.map do |key| + klass.column_types.fetch(key) { + result.column_types.fetch(key) { + Class.new { def type_cast(v); v; end }.new + } + } + end - value = klass.initialize_attributes(attributes).values.first + result = result.map do |attributes| + values = klass.initialize_attributes(attributes).values - column.type_cast(value) + columns.zip(values).map do |column, value| + column.type_cast(value) + end + end + columns.one? ? result.map!(&:first) : result end end @@ -185,6 +197,10 @@ module ActiveRecord private + def has_include?(column_name) + eager_loading? || (includes_values.present? && (column_name || references_eager_loaded_tables?)) + end + def perform_calculation(operation, column_name, options = {}) operation = operation.to_s.downcase @@ -245,10 +261,16 @@ module ActiveRecord end def execute_grouped_calculation(operation, column_name, distinct) #:nodoc: - group_attr = group_values - association = @klass.reflect_on_association(group_attr.first.to_sym) - associated = group_attr.size == 1 && association && association.macro == :belongs_to # only count belongs_to associations - group_fields = Array(associated ? association.foreign_key : group_attr) + group_attrs = group_values + + if group_attrs.first.respond_to?(:to_sym) + association = @klass.reflect_on_association(group_attrs.first.to_sym) + associated = group_attrs.size == 1 && association && association.macro == :belongs_to # only count belongs_to associations + group_fields = Array(associated ? association.foreign_key : group_attrs) + else + group_fields = group_attrs + end + group_aliases = group_fields.map { |field| column_alias_for(field) } group_columns = group_aliases.zip(group_fields).map { |aliaz,field| [aliaz, column_for(field)] @@ -271,10 +293,14 @@ module ActiveRecord select_values += select_values unless having_values.empty? select_values.concat group_fields.zip(group_aliases).map { |field,aliaz| - "#{field} AS #{aliaz}" + if field.respond_to?(:as) + field.as(aliaz) + else + "#{field} AS #{aliaz}" + end } - relation = except(:group).group(group.join(',')) + relation = except(:group).group(group) relation.select_values = select_values calculated_data = @klass.connection.select_all(relation, nil, bind_values) @@ -286,10 +312,10 @@ module ActiveRecord end Hash[calculated_data.map do |row| - key = group_columns.map { |aliaz, column| + key = group_columns.map { |aliaz, column| type_cast_calculated_value(row[aliaz], column) } - key = key.first if key.size == 1 + key = key.first if key.size == 1 key = key_records[key] if associated [key, type_cast_calculated_value(row[aggregate_alias], column_for(column_name), operation)] end] @@ -304,6 +330,7 @@ module ActiveRecord # column_alias_for("count(*)") # => "count_all" # column_alias_for("count", "id") # => "count_id" def column_alias_for(*keys) + keys.map! {|k| k.respond_to?(:to_sql) ? k.to_sql : k} table_name = keys.join(' ') table_name.downcase! table_name.gsub!(/\*/, 'all') @@ -315,7 +342,7 @@ module ActiveRecord end def column_for(field) - field_name = field.to_s.split('.').last + field_name = field.respond_to?(:name) ? field.name.to_s : field.to_s.split('.').last @klass.columns.detect { |c| c.name.to_s == field_name } end diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb index f5fdf437bf..ab8b36c8ab 100644 --- a/activerecord/lib/active_record/relation/delegation.rb +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -1,7 +1,6 @@ -require 'active_support/core_ext/module/delegation' module ActiveRecord - module Delegation + module Delegation # :nodoc: # Set up common delegations for performance (avoids method_missing) delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to_ary, :to => :to_a delegate :table_name, :quoted_table_name, :primary_key, :quoted_primary_key, @@ -32,12 +31,12 @@ module ActiveRecord protected def method_missing(method, *args, &block) - if Array.method_defined?(method) - ::ActiveRecord::Delegation.delegate method, :to => :to_a - to_a.send(method, *args, &block) - elsif @klass.respond_to?(method) + if @klass.respond_to?(method) ::ActiveRecord::Delegation.delegate_to_scoped_klass(method) scoping { @klass.send(method, *args, &block) } + elsif Array.method_defined?(method) + ::ActiveRecord::Delegation.delegate method, :to => :to_a + to_a.send(method, *args, &block) elsif arel.respond_to?(method) ::ActiveRecord::Delegation.delegate method, :to => :arel arel.send(method, *args, &block) @@ -46,4 +45,4 @@ module ActiveRecord end end end -end
\ No newline at end of file +end diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 5f6898b45a..84aaa39fed 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/object/blank' require 'active_support/core_ext/hash/indifferent_access' module ActiveRecord @@ -106,7 +105,7 @@ module ActiveRecord # Person.last # returns the last object fetched by SELECT * FROM people # Person.where(["user_name = ?", user_name]).last # Person.order("created_on DESC").offset(5).last - # Person.last(3) # returns the last three objects fetched by SELECT * FROM people. + # Person.last(3) # returns the last three objects fetched by SELECT * FROM people. # # Take note that in that last case, the results are sorted in ascending order: # @@ -133,21 +132,8 @@ module ActiveRecord last or raise RecordNotFound end - # Runs the query on the database and returns records with the used query - # methods. - # - # Person.all # returns an array of objects for all the rows fetched by SELECT * FROM people - # Person.where(["category IN (?)", categories]).limit(50).all - # Person.where({ :friends => ["Bob", "Steve", "Fred"] }).all - # Person.offset(10).limit(10).all - # Person.includes([:account, :friends]).all - # Person.group("category").all - def all - to_a - end - - # Returns true if a record exists in the table that matches the +id+ or - # conditions given, or false otherwise. The argument can take five forms: + # Returns +true+ if a record exists in the table that matches the +id+ or + # conditions given, or +false+ otherwise. The argument can take six forms: # # * Integer - Finds the record with this primary key. # * String - Finds the record with a primary key corresponding to this @@ -155,8 +141,9 @@ module ActiveRecord # * Array - Finds the record that matches these +find+-style conditions # (such as <tt>['color = ?', 'red']</tt>). # * Hash - Finds the record that matches these +find+-style conditions - # (such as <tt>{:color => 'red'}</tt>). - # * No args - Returns false if the table is empty, true otherwise. + # (such as <tt>{color: 'red'}</tt>). + # * +false+ - Returns always +false+. + # * No args - Returns +false+ if the table is empty, +true+ otherwise. # # For more information about specifying conditions as a Hash or Array, # see the Conditions section in the introduction to ActiveRecord::Base. @@ -168,24 +155,27 @@ module ActiveRecord # Person.exists?(5) # Person.exists?('5') # Person.exists?(['name LIKE ?', "%#{query}%"]) - # Person.exists?(:name => "David") + # Person.exists?(name: 'David') + # Person.exists?(false) # Person.exists? - def exists?(id = false) - id = id.id if ActiveRecord::Model === id - return false if id.nil? + def exists?(conditions = :none) + conditions = conditions.id if ActiveRecord::Model === conditions + return false if !conditions join_dependency = construct_join_dependency_for_association_find relation = construct_relation_for_association_find(join_dependency) - relation = relation.except(:select, :order).select("1").limit(1) + relation = relation.except(:select, :order).select("1 AS one").limit(1) - case id + case conditions when Array, Hash - relation = relation.where(id) + relation = relation.where(conditions) else - relation = relation.where(table[primary_key].eq(id)) if id + relation = relation.where(table[primary_key].eq(conditions)) if conditions != :none end connection.select_value(relation, "#{name} Exists", relation.bind_values) + rescue ThrowResult + false end protected @@ -281,7 +271,7 @@ module ActiveRecord end def find_some(ids) - result = where(table[primary_key].in(ids)).all + result = where(table[primary_key].in(ids)).to_a expected_size = if limit_value && ids.size > limit_value @@ -320,7 +310,7 @@ module ActiveRecord @records.first else @first ||= - if order_values.empty? && primary_key + if with_default_scope.order_values.empty? && primary_key order(arel_table[primary_key].asc).limit(1).to_a.first else limit(1).to_a.first diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb index 36f98c6480..e5b50673da 100644 --- a/activerecord/lib/active_record/relation/merger.rb +++ b/activerecord/lib/active_record/relation/merger.rb @@ -1,9 +1,8 @@ -require 'active_support/core_ext/object/blank' require 'active_support/core_ext/hash/keys' module ActiveRecord class Relation - class HashMerger + class HashMerger # :nodoc: attr_reader :relation, :hash def initialize(relation, hash) @@ -28,7 +27,7 @@ module ActiveRecord end end - class Merger + class Merger # :nodoc: attr_reader :relation, :values def initialize(relation, other) @@ -98,15 +97,13 @@ module ActiveRecord merged_wheres = relation.where_values + values[:where] unless relation.where_values.empty? - # Remove duplicates, last one wins. - seen = Hash.new { |h,table| h[table] = {} } + # Remove equalities with duplicated left-hand. Last one wins. + seen = {} merged_wheres = merged_wheres.reverse.reject { |w| nuke = false if w.respond_to?(:operator) && w.operator == :== - name = w.left.name - table = w.left.relation.name - nuke = seen[table][name] - seen[table][name] = true + nuke = seen[w.left] + seen[w.left] = true end nuke }.reverse diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index a89d0f3ebf..8e6254f918 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -1,5 +1,4 @@ require 'active_support/core_ext/array/wrap' -require 'active_support/core_ext/object/blank' module ActiveRecord module QueryMethods @@ -7,42 +6,67 @@ module ActiveRecord Relation::MULTI_VALUE_METHODS.each do |name| class_eval <<-CODE, __FILE__, __LINE__ + 1 - def #{name}_values # def select_values - @values[:#{name}] || [] # @values[:select] || [] - end # end - # - def #{name}_values=(values) # def select_values=(values) - @values[:#{name}] = values # @values[:select] = values - end # end + def #{name}_values # def select_values + @values[:#{name}] || [] # @values[:select] || [] + end # end + # + def #{name}_values=(values) # def select_values=(values) + raise ImmutableRelation if @loaded # raise ImmutableRelation if @loaded + @values[:#{name}] = values # @values[:select] = values + end # end CODE end (Relation::SINGLE_VALUE_METHODS - [:create_with]).each do |name| class_eval <<-CODE, __FILE__, __LINE__ + 1 - def #{name}_value # def readonly_value - @values[:#{name}] # @values[:readonly] - end # end - # - def #{name}_value=(value) # def readonly_value=(value) - @values[:#{name}] = value # @values[:readonly] = value - end # end + def #{name}_value # def readonly_value + @values[:#{name}] # @values[:readonly] + end # end CODE end - def create_with_value - @values[:create_with] || {} + Relation::SINGLE_VALUE_METHODS.each do |name| + class_eval <<-CODE, __FILE__, __LINE__ + 1 + def #{name}_value=(value) # def readonly_value=(value) + raise ImmutableRelation if @loaded # raise ImmutableRelation if @loaded + @values[:#{name}] = value # @values[:readonly] = value + end # end + CODE end - def create_with_value=(value) - @values[:create_with] = value + def create_with_value # :nodoc: + @values[:create_with] || {} end alias extensions extending_values + # Specify relationships to be included in the result set. For + # example: + # + # users = User.includes(:address) + # users.each do |user| + # user.address.city + # end + # + # allows you to access the +address+ attribute of the +User+ model without + # firing an additional query. This will often result in a + # performance improvement over a simple +join+. + # + # === conditions + # + # If you want to add conditions to your included models you'll have + # to explicitly reference them. For example: + # + # User.includes(:posts).where('posts.name = ?', 'example') + # + # Will throw an error, but this will work: + # + # User.includes(:posts).where('posts.name = ?', 'example').references(:posts) def includes(*args) args.empty? ? self : spawn.includes!(*args) end + # Like #includes, but modifies the relation in place. def includes!(*args) args.reject! {|a| a.blank? } @@ -50,19 +74,31 @@ module ActiveRecord self end + # Forces eager loading by performing a LEFT OUTER JOIN on +args+: + # + # User.eager_load(:posts) + # => SELECT "users"."id" AS t0_r0, "users"."name" AS t0_r1, ... + # FROM "users" LEFT OUTER JOIN "posts" ON "posts"."user_id" = + # "users"."id" def eager_load(*args) args.blank? ? self : spawn.eager_load!(*args) end + # Like #eager_load, but modifies relation in place. def eager_load!(*args) self.eager_load_values += args self end + # Allows preloading of +args+, in the same way that +includes+ does: + # + # User.preload(:posts) + # => SELECT "posts".* FROM "posts" WHERE "posts"."user_id" IN (1, 2, 3) def preload(*args) args.blank? ? self : spawn.preload!(*args) end + # Like #preload, but modifies relation in place. def preload!(*args) self.preload_values += args self @@ -71,8 +107,6 @@ module ActiveRecord # Used to indicate that an association is referenced by an SQL string, and should # therefore be JOINed in any query rather than loaded separately. # - # For example: - # # User.includes(:posts).where("posts.name = 'foo'") # # => Doesn't JOIN the posts table, resulting in an error. # @@ -82,8 +116,11 @@ module ActiveRecord args.blank? ? self : spawn.references!(*args) end + # Like #references, but modifies relation in place. def references!(*args) - self.references_values = (references_values + args.flatten.map(&:to_s)).uniq + args.flatten! + + self.references_values = (references_values + args.map!(&:to_s)).uniq self end @@ -91,7 +128,7 @@ module ActiveRecord # # First: takes a block so it can be used just like Array#select. # - # Model.scoped.select { |m| m.field == value } + # Model.all.select { |m| m.field == value } # # This will build an array of objects from the database for the scope, # converting them into an array and iterating through them using Array#select. @@ -124,33 +161,59 @@ module ActiveRecord end end + # Like #select, but modifies relation in place. def select!(value) self.select_values += Array.wrap(value) self end + # Allows to specify a group attribute: + # + # User.group(:name) + # => SELECT "users".* FROM "users" GROUP BY name + # + # Returns an array with distinct records based on the +group+ attribute: + # + # User.select([:id, :name]) + # => [#<User id: 1, name: "Oscar">, #<User id: 2, name: "Oscar">, #<User id: 3, name: "Foo"> + # + # User.group(:name) + # => [#<User id: 3, name: "Foo", ...>, #<User id: 2, name: "Oscar", ...>] def group(*args) args.blank? ? self : spawn.group!(*args) end + # Like #group, but modifies relation in place. def group!(*args) - self.group_values += args.flatten + args.flatten! + + self.group_values += args self end + # Allows to specify an order attribute: + # + # User.order('name') + # => SELECT "users".* FROM "users" ORDER BY name + # + # User.order('name DESC') + # => SELECT "users".* FROM "users" ORDER BY name DESC + # + # User.order('name DESC, email') + # => SELECT "users".* FROM "users" ORDER BY name DESC, email def order(*args) args.blank? ? self : spawn.order!(*args) end + # Like #order, but modifies relation in place. def order!(*args) - args = args.flatten + args.flatten! references = args.reject { |arg| Arel::Node === arg } - .map { |arg| arg =~ /^([a-zA-Z]\w*)\.(\w+)/ && $1 } - .compact + references.map! { |arg| arg =~ /^([a-zA-Z]\w*)\.(\w+)/ && $1 }.compact! references!(references) if references.any? - self.order_values += args + self.order_values = args + self.order_values self end @@ -162,22 +225,29 @@ module ActiveRecord # # User.order('email DESC').reorder('id ASC').order('name ASC') # - # generates a query with 'ORDER BY id ASC, name ASC'. - # + # generates a query with 'ORDER BY name ASC, id ASC'. def reorder(*args) args.blank? ? self : spawn.reorder!(*args) end + # Like #reorder, but modifies relation in place. def reorder!(*args) + args.flatten! + self.reordering_value = true - self.order_values = args.flatten + self.order_values = args self end + # Performs a joins on +args+: + # + # User.joins(:posts) + # => SELECT "users".* FROM "users" INNER JOIN "posts" ON "posts"."user_id" = "users"."id" def joins(*args) args.compact.blank? ? self : spawn.joins!(*args) end + # Like #joins, but modifies relation in place. def joins!(*args) args.flatten! @@ -194,10 +264,102 @@ module ActiveRecord self end + # Returns a new relation, which is the result of filtering the current relation + # according to the conditions in the arguments. + # + # #where accepts conditions in one of several formats. In the examples below, the resulting + # SQL is given as an illustration; the actual query generated may be different depending + # on the database adapter. + # + # === string + # + # A single string, without additional arguments, is passed to the query + # constructor as a SQL fragment, and used in the where clause of the query. + # + # Client.where("orders_count = '2'") + # # SELECT * from clients where orders_count = '2'; + # + # Note that building your own string from user input may expose your application + # to injection attacks if not done properly. As an alternative, it is recommended + # to use one of the following methods. + # + # === array + # + # If an array is passed, then the first element of the array is treated as a template, and + # the remaining elements are inserted into the template to generate the condition. + # Active Record takes care of building the query to avoid injection attacks, and will + # convert from the ruby type to the database type where needed. Elements are inserted + # into the string in the order in which they appear. + # + # User.where(["name = ? and email = ?", "Joe", "joe@example.com"]) + # # SELECT * FROM users WHERE name = 'Joe' AND email = 'joe@example.com'; + # + # Alternatively, you can use named placeholders in the template, and pass a hash as the + # second element of the array. The names in the template are replaced with the corresponding + # values from the hash. + # + # User.where(["name = :name and email = :email", { name: "Joe", email: "joe@example.com" }]) + # # SELECT * FROM users WHERE name = 'Joe' AND email = 'joe@example.com'; + # + # This can make for more readable code in complex queries. + # + # Lastly, you can use sprintf-style % escapes in the template. This works slightly differently + # than the previous methods; you are responsible for ensuring that the values in the template + # are properly quoted. The values are passed to the connector for quoting, but the caller + # is responsible for ensuring they are enclosed in quotes in the resulting SQL. After quoting, + # the values are inserted using the same escapes as the Ruby core method <tt>Kernel::sprintf</tt>. + # + # User.where(["name = '%s' and email = '%s'", "Joe", "joe@example.com"]) + # # SELECT * FROM users WHERE name = 'Joe' AND email = 'joe@example.com'; + # + # If #where is called with multiple arguments, these are treated as if they were passed as + # the elements of a single array. + # + # User.where("name = :name and email = :email", { name: "Joe", email: "joe@example.com" }) + # # SELECT * FROM users WHERE name = 'Joe' AND email = 'joe@example.com'; + # + # When using strings to specify conditions, you can use any operator available from + # the database. While this provides the most flexibility, you can also unintentionally introduce + # dependencies on the underlying database. If your code is intended for general consumption, + # test with multiple database backends. + # + # === hash + # + # #where will also accept a hash condition, in which the keys are fields and the values + # are values to be searched for. + # + # Fields can be symbols or strings. Values can be single values, arrays, or ranges. + # + # User.where({ name: "Joe", email: "joe@example.com" }) + # # SELECT * FROM users WHERE name = 'Joe' AND email = 'joe@example.com' + # + # User.where({ name: ["Alice", "Bob"]}) + # # SELECT * FROM users WHERE name IN ('Alice', 'Bob') + # + # User.where({ created_at: (Time.now.midnight - 1.day)..Time.now.midnight }) + # # SELECT * FROM users WHERE (created_at BETWEEN '2012-06-09 07:00:00.000000' AND '2012-06-10 07:00:00.000000') + # + # === Joins + # + # If the relation is the result of a join, you may create a condition which uses any of the + # tables in the join. For string and array conditions, use the table name in the condition. + # + # User.joins(:posts).where("posts.created_at < ?", Time.now) + # + # For hash conditions, you can either use the table name in the key, or use a sub-hash. + # + # User.joins(:posts).where({ "posts.published" => true }) + # User.joins(:posts).where({ :posts => { :published => true } }) + # + # === empty condition + # + # If the condition returns true for blank?, then where is a no-op and returns the current relation. def where(opts, *rest) opts.blank? ? self : spawn.where!(opts, *rest) end + # #where! is identical to #where, except that instead of returning a new relation, it adds + # the condition to the existing relation. def where!(opts, *rest) references!(PredicateBuilder.references(opts)) if Hash === opts @@ -205,10 +367,15 @@ module ActiveRecord self end + # Allows to specify a HAVING clause. Note that you can't use HAVING + # without also specifying a GROUP clause. + # + # Order.having('SUM(price) > 30').group('user_id') def having(opts, *rest) opts.blank? ? self : spawn.having!(opts, *rest) end + # Like #having, but modifies relation in place. def having!(opts, *rest) references!(PredicateBuilder.references(opts)) if Hash === opts @@ -216,28 +383,45 @@ module ActiveRecord self end + # Specifies a limit for the number of records to retrieve. + # + # User.limit(10) # generated SQL has 'LIMIT 10' + # + # User.limit(10).limit(20) # generated SQL has 'LIMIT 20' def limit(value) spawn.limit!(value) end + # Like #limit, but modifies relation in place. def limit!(value) self.limit_value = value self end + # Specifies the number of rows to skip before returning rows. + # + # User.offset(10) # generated SQL has "OFFSET 10" + # + # Should be used with order. + # + # User.offset(10).order("name ASC") def offset(value) spawn.offset!(value) end + # Like #offset, but modifies relation in place. def offset!(value) self.offset_value = value self end + # Specifies locking settings (default to +true+). For more information + # on locking, please see +ActiveRecord::Locking+. def lock(locks = true) spawn.lock!(locks) end + # Like #lock, but modifies relation in place. def lock!(locks = true) case locks when String, TrueClass, NilClass @@ -250,11 +434,11 @@ module ActiveRecord end # Returns a chainable relation with zero records, specifically an - # instance of the NullRelation class. + # instance of the <tt>ActiveRecord::NullRelation</tt> class. # - # The returned NullRelation inherits from Relation and implements the - # Null Object pattern so it is an object with defined null behavior: - # it always returns an empty array of records and does not query the database. + # The returned <tt>ActiveRecord::NullRelation</tt> inherits from Relation and implements the + # Null Object pattern. It is an object with defined null behavior and always returns an empty + # array of records without quering the database. # # Any subsequent condition chained to the returned relation will continue # generating an empty relation and will not fire any query to the database. @@ -279,22 +463,47 @@ module ActiveRecord # end # def none - scoped.extending(NullRelation) + extending(NullRelation) end + # Sets readonly attributes for the returned relation. If value is + # true (default), attempting to update a record will result in an error. + # + # users = User.readonly + # users.first.save + # => ActiveRecord::ReadOnlyRecord: ActiveRecord::ReadOnlyRecord def readonly(value = true) spawn.readonly!(value) end + # Like #readonly, but modifies relation in place. def readonly!(value = true) self.readonly_value = value self end + # Sets attributes to be used when creating new records from a + # relation object. + # + # users = User.where(name: 'Oscar') + # users.new.name # => 'Oscar' + # + # users = users.create_with(name: 'DHH') + # users.new.name # => 'DHH' + # + # You can pass +nil+ to +create_with+ to reset attributes: + # + # users = users.create_with(nil) + # users.new.name # => 'Oscar' def create_with(value) spawn.create_with!(value) end + # Like #create_with but modifies the relation in place. Raises + # +ImmutableRelation+ if the relation has already been loaded. + # + # users = User.all.create_with!(name: 'Oscar') + # users.new.name # => 'Oscar' def create_with!(value) self.create_with_value = value ? create_with_value.merge(value) : {} self @@ -307,16 +516,17 @@ module ActiveRecord # # Can accept other relation objects. For example: # - # Topic.select('title').from(Topics.approved) + # Topic.select('title').from(Topic.approved) # # => SELECT title FROM (SELECT * FROM topics WHERE approved = 't') subquery # - # Topics.select('a.title').from(Topics.approved, :a) + # Topic.select('a.title').from(Topic.approved, :a) # # => SELECT a.title FROM (SELECT * FROM topics WHERE approved = 't') a # def from(value, subquery_name = nil) spawn.from!(value, subquery_name) end + # Like #from, but modifies relation in place. def from!(value, subquery_name = nil) self.from_value = [value, subquery_name] self @@ -336,6 +546,7 @@ module ActiveRecord spawn.uniq!(value) end + # Like #uniq, but modifies relation in place. def uniq!(value = true) self.uniq_value = value self @@ -354,16 +565,16 @@ module ActiveRecord # end # end # - # scope = Model.scoped.extending(Pagination) + # scope = Model.all.extending(Pagination) # scope.page(params[:page]) # # You can also pass a list of modules: # - # scope = Model.scoped.extending(Pagination, SomethingElse) + # scope = Model.all.extending(Pagination, SomethingElse) # # === Using a block # - # scope = Model.scoped.extending do + # scope = Model.all.extending do # def page(number) # # pagination code goes here # end @@ -372,7 +583,7 @@ module ActiveRecord # # You can also use a block and a module list: # - # scope = Model.scoped.extending(Pagination) do + # scope = Model.all.extending(Pagination) do # def per_page(number) # # pagination code goes here # end @@ -385,30 +596,37 @@ module ActiveRecord end end + # Like #extending, but modifies relation in place. def extending!(*modules, &block) modules << Module.new(&block) if block_given? - self.extending_values = modules.flatten + self.extending_values += modules.flatten extend(*extending_values) if extending_values.any? self end + # Reverse the existing order clause on the relation. + # + # User.order('name ASC').reverse_order # generated SQL has 'ORDER BY name DESC' def reverse_order spawn.reverse_order! end + # Like #reverse_order, but modifies relation in place. def reverse_order! self.reverse_order_value = !reverse_order_value self end + # Returns the Arel object associated with the relation. def arel @arel ||= with_default_scope.build_arel end + # Like #arel, but ignores the default scope of the model. def build_arel - arel = table.from table + arel = Arel::SelectManager.new(table.engine, table) build_joins(arel, joins_values) unless joins_values.empty? diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index 80d087a9ea..5394c1b28b 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/object/blank' require 'active_support/core_ext/hash/except' require 'active_support/core_ext/hash/slice' require 'active_record/relation/merger' @@ -24,6 +23,13 @@ module ActiveRecord # # Returns the intersection of all published posts with the 5 most recently created posts. # # (This is just an example. You'd probably want to do this with a single query!) # + # Procs will be evaluated by merge: + # + # Post.where(published: true).merge(-> { joins(:comments) }) + # # => Post.where(published: true).joins(:comments) + # + # This is mainly intended for sharing common conditions between multiple associations. + # def merge(other) if other.is_a?(Array) to_a & other @@ -34,9 +40,14 @@ module ActiveRecord end end + # Like #merge, but applies changes in place. def merge!(other) - klass = other.is_a?(Hash) ? Relation::HashMerger : Relation::Merger - klass.new(self, other).merge + if !other.is_a?(Relation) && other.respond_to?(:to_proc) + instance_exec(&other) + else + klass = other.is_a?(Hash) ? Relation::HashMerger : Relation::Merger + klass.new(self, other).merge + end end # Removes from the query the condition(s) specified in +skips+. diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb index fd276ccf5d..2414a4bbd7 100644 --- a/activerecord/lib/active_record/result.rb +++ b/activerecord/lib/active_record/result.rb @@ -10,11 +10,11 @@ module ActiveRecord attr_reader :columns, :rows, :column_types - def initialize(columns, rows) + def initialize(columns, rows, column_types = {}) @columns = columns @rows = rows @hash_rows = nil - @column_types = {} + @column_types = column_types end def each diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index 5530be3219..690409d62c 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -1,4 +1,3 @@ -require 'active_support/concern' module ActiveRecord module Sanitization @@ -180,15 +179,8 @@ module ActiveRecord end # TODO: Deprecate this - def quoted_id #:nodoc: - quote_value(id, column_for_attribute(self.class.primary_key)) - end - - private - - # Quote strings appropriately for SQL statements. - def quote_value(value, column = nil) - self.class.connection.quote(value, column) + def quoted_id + self.class.quote_value(id, column_for_attribute(self.class.primary_key)) end end end diff --git a/activerecord/lib/active_record/schema.rb b/activerecord/lib/active_record/schema.rb index 599e68379a..a540bc0a3b 100644 --- a/activerecord/lib/active_record/schema.rb +++ b/activerecord/lib/active_record/schema.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/object/blank' module ActiveRecord # = Active Record Schema diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index 7cbe2db408..a25a8d79bd 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -70,8 +70,8 @@ HEADER @connection.tables.sort.each do |tbl| next if ['schema_migrations', ignore_tables].flatten.any? do |ignored| case ignored - when String; tbl == ignored - when Regexp; tbl =~ ignored + when String; remove_prefix_and_suffix(tbl) == ignored + when Regexp; remove_prefix_and_suffix(tbl) =~ ignored else raise StandardError, 'ActiveRecord::SchemaDumper.ignore_tables accepts an array of String and / or Regexp values.' end @@ -92,7 +92,7 @@ HEADER pk = @connection.primary_key(table) end - tbl.print " create_table #{table.inspect}" + tbl.print " create_table #{remove_prefix_and_suffix(table).inspect}" if columns.detect { |c| c.name == pk } if pk != 'id' tbl.print %Q(, :primary_key => "#{pk}") @@ -175,7 +175,7 @@ HEADER when BigDecimal value.to_s when Date, DateTime, Time - "'" + value.to_s(:db) + "'" + "'#{value.to_s(:db)}'" else value.inspect end @@ -185,7 +185,7 @@ HEADER if (indexes = @connection.indexes(table)).any? add_index_statements = indexes.map do |index| statement_parts = [ - ('add_index ' + index.table.inspect), + ('add_index ' + remove_prefix_and_suffix(index.table).inspect), index.columns.inspect, (':name => ' + index.name.inspect), ] @@ -206,5 +206,9 @@ HEADER stream.puts end end + + def remove_prefix_and_suffix(table) + table.gsub(/^(#{ActiveRecord::Base.table_name_prefix})(.+)(#{ActiveRecord::Base.table_name_suffix})$/, "\\2") + end end end diff --git a/activerecord/lib/active_record/schema_migration.rb b/activerecord/lib/active_record/schema_migration.rb index 236ec563d2..ca22154c84 100644 --- a/activerecord/lib/active_record/schema_migration.rb +++ b/activerecord/lib/active_record/schema_migration.rb @@ -7,7 +7,11 @@ module ActiveRecord attr_accessible :version def self.table_name - Base.table_name_prefix + 'schema_migrations' + Base.table_name_suffix + "#{Base.table_name_prefix}schema_migrations#{Base.table_name_suffix}" + end + + def self.index_name + "#{Base.table_name_prefix}unique_schema_migrations#{Base.table_name_suffix}" end def self.create_table @@ -15,14 +19,13 @@ module ActiveRecord connection.create_table(table_name, :id => false) do |t| t.column :version, :string, :null => false end - connection.add_index table_name, :version, :unique => true, - :name => "#{Base.table_name_prefix}unique_schema_migrations#{Base.table_name_suffix}" + connection.add_index table_name, :version, :unique => true, :name => index_name end end def self.drop_table if connection.table_exists?(table_name) - connection.remove_index table_name, :name => "#{Base.table_name_prefix}unique_schema_migrations#{Base.table_name_suffix}" + connection.remove_index table_name, :name => index_name connection.drop_table(table_name) end end diff --git a/activerecord/lib/active_record/scoping.rb b/activerecord/lib/active_record/scoping.rb index 66a486ae0a..0c3fd1bd29 100644 --- a/activerecord/lib/active_record/scoping.rb +++ b/activerecord/lib/active_record/scoping.rb @@ -1,4 +1,3 @@ -require 'active_support/concern' module ActiveRecord module Scoping diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb index db833fc7f1..a2a85d4b96 100644 --- a/activerecord/lib/active_record/scoping/default.rb +++ b/activerecord/lib/active_record/scoping/default.rb @@ -1,5 +1,3 @@ -require 'active_support/concern' -require 'active_support/deprecation' module ActiveRecord module Scoping @@ -8,7 +6,7 @@ module ActiveRecord included do # Stores the default scope for the class - config_attribute :default_scopes + class_attribute :default_scopes, instance_writer: false self.default_scopes = [] end @@ -31,14 +29,14 @@ module ActiveRecord # Post.limit(10) # Fires "SELECT * FROM posts LIMIT 10" # } # - # It is recommended that you use the block form of unscoped because chaining - # unscoped with <tt>scope</tt> does not work. Assuming that + # It is recommended that you use the block form of unscoped because + # chaining unscoped with <tt>scope</tt> does not work. Assuming that # <tt>published</tt> is a <tt>scope</tt>, the following two statements - # are equal: the default_scope is applied on both. + # are equal: the <tt>default_scope</tt> is applied on both. # # Post.unscoped.published # Post.published - def unscoped #:nodoc: + def unscoped block_given? ? relation.scoping { yield } : relation end diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb index 2af476c1ba..75f31229b5 100644 --- a/activerecord/lib/active_record/scoping/named.rb +++ b/activerecord/lib/active_record/scoping/named.rb @@ -1,9 +1,6 @@ require 'active_support/core_ext/array' require 'active_support/core_ext/hash/except' require 'active_support/core_ext/kernel/singleton_class' -require 'active_support/core_ext/object/blank' -require 'active_support/core_ext/class/attribute' -require 'active_support/deprecation' module ActiveRecord # = Active Record Named \Scopes @@ -12,33 +9,26 @@ module ActiveRecord extend ActiveSupport::Concern module ClassMethods - # Returns an anonymous \scope. + # Returns an <tt>ActiveRecord::Relation</tt> scope object. # - # posts = Post.scoped + # posts = Post.all # posts.size # Fires "select count(*) from posts" and returns the count # posts.each {|p| puts p.name } # Fires "select * from posts" and loads post objects # - # fruits = Fruit.scoped + # fruits = Fruit.all # 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 - # queries, where passing intermediate values (\scopes) around as first-class - # objects is convenient. - # # You can define a \scope that applies to all finders using # ActiveRecord::Base.default_scope. - def scoped(options = nil) + def all if current_scope - scope = current_scope.clone + current_scope.clone else scope = relation scope.default_scoped = true scope end - - scope.merge!(options) if options - scope end ## @@ -189,7 +179,7 @@ module ActiveRecord singleton_class.send(:define_method, name) do |*args| options = body.respond_to?(:call) ? unscoped { body.call(*args) } : body - relation = scoped.merge(options) + relation = all.merge(options) extension ? relation.extending(extension) : relation end diff --git a/activerecord/lib/active_record/serialization.rb b/activerecord/lib/active_record/serialization.rb index 41e3b92499..e8dd312a47 100644 --- a/activerecord/lib/active_record/serialization.rb +++ b/activerecord/lib/active_record/serialization.rb @@ -1,9 +1,21 @@ module ActiveRecord #:nodoc: + ActiveSupport.on_load(:active_record_config) do + mattr_accessor :include_root_in_json, instance_accessor: false + self.include_root_in_json = true + end + # = Active Record Serialization module Serialization extend ActiveSupport::Concern include ActiveModel::Serializers::JSON + included do + singleton_class.class_eval do + remove_method :include_root_in_json + delegate :include_root_in_json, to: 'ActiveRecord::Model' + end + end + def serializable_hash(options = nil) options = options.try(:clone) || {} diff --git a/activerecord/lib/active_record/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb index b833af64fe..834d01a1e8 100644 --- a/activerecord/lib/active_record/serializers/xml_serializer.rb +++ b/activerecord/lib/active_record/serializers/xml_serializer.rb @@ -177,11 +177,6 @@ module ActiveRecord #:nodoc: end class XmlSerializer < ActiveModel::Serializers::Xml::Serializer #:nodoc: - def initialize(*args) - super - options[:except] = Array(options[:except]) | Array(@serializable.class.inheritance_column) - end - class Attribute < ActiveModel::Serializers::Xml::Serializer::Attribute #:nodoc: def compute_type klass = @serializable.class diff --git a/activerecord/lib/active_record/session_store.rb b/activerecord/lib/active_record/session_store.rb index 5a256b040b..58e1dab508 100644 --- a/activerecord/lib/active_record/session_store.rb +++ b/activerecord/lib/active_record/session_store.rb @@ -1,3 +1,5 @@ +require 'action_dispatch/middleware/session/abstract_store' + module ActiveRecord # = Active Record Session Store # @@ -7,7 +9,7 @@ module ActiveRecord # # The default assumes a +sessions+ tables with columns: # +id+ (numeric primary key), - # +session_id+ (text, or longtext if your session data exceeds 65K), and + # +session_id+ (string, usually varchar; maximum length is 255), and # +data+ (text or longtext; careful if your session data exceeds 65KB). # # The +session_id+ column should always be indexed for speedy lookups. @@ -218,7 +220,7 @@ module ActiveRecord # Look up a session by id and unmarshal its data if found. def find_by_session_id(session_id) - if record = connection.select_one("SELECT * FROM #{@@table_name} WHERE #{@@session_id_column}=#{connection.quote(session_id.to_s)}") + if record = connection.select_one("SELECT #{connection.quote_column_name(data_column)} AS data FROM #{@@table_name} WHERE #{connection.quote_column_name(@@session_id_column)}=#{connection.quote(session_id.to_s)}") new(:session_id => session_id, :marshaled_data => record['data']) end end diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb index d70e02e379..b4013ecc1e 100644 --- a/activerecord/lib/active_record/store.rb +++ b/activerecord/lib/active_record/store.rb @@ -2,7 +2,7 @@ require 'active_support/core_ext/hash/indifferent_access' module ActiveRecord # Store gives you a thin wrapper around serialize for the purpose of storing hashes in a single column. - # It's like a simple key/value store backed into your record when you don't care about being able to + # It's like a simple key/value store baked into your record when you don't care about being able to # query that store outside the context of a single record. # # You can then declare accessors to this store that are then accessible just like any other attribute @@ -33,9 +33,18 @@ module ActiveRecord # class SuperUser < User # store_accessor :settings, :privileges, :servants # end + # + # The stored attribute names can be retrieved using +stored_attributes+. + # + # User.stored_attributes[:settings] # [:color, :homepage] module Store extend ActiveSupport::Concern + included do + class_attribute :stored_attributes, instance_accessor: false + self.stored_attributes = {} + end + module ClassMethods def store(store_attribute, options = {}) serialize store_attribute, IndifferentCoder.new(options[:coder]) @@ -43,33 +52,33 @@ module ActiveRecord end def store_accessor(store_attribute, *keys) - keys.flatten.each do |key| + keys = keys.flatten + keys.each do |key| define_method("#{key}=") do |value| - initialize_store_attribute(store_attribute) - send(store_attribute)[key] = value - send :"#{store_attribute}_will_change!" + attribute = initialize_store_attribute(store_attribute) + if value != attribute[key] + attribute[key] = value + send :"#{store_attribute}_will_change!" + end end define_method(key) do - initialize_store_attribute(store_attribute) - send(store_attribute)[key] + initialize_store_attribute(store_attribute)[key] end end + + self.stored_attributes[store_attribute] = keys end end private def initialize_store_attribute(store_attribute) - case attribute = send(store_attribute) - when ActiveSupport::HashWithIndifferentAccess - # Already initialized. Do nothing. - when Hash - # Initialized as a Hash. Convert to indifferent access. - send :"#{store_attribute}=", attribute.with_indifferent_access - else - # Uninitialized. Set to an indifferent hash. - send :"#{store_attribute}=", ActiveSupport::HashWithIndifferentAccess.new + attribute = send(store_attribute) + unless attribute.is_a?(HashWithIndifferentAccess) + attribute = IndifferentCoder.as_indifferent_hash(attribute) + send :"#{store_attribute}=", attribute end + attribute end class IndifferentCoder @@ -92,7 +101,7 @@ module ActiveRecord def self.as_indifferent_hash(obj) case obj - when ActiveSupport::HashWithIndifferentAccess + when HashWithIndifferentAccess obj when Hash obj.with_indifferent_access diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb new file mode 100644 index 0000000000..b41cc68b6a --- /dev/null +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -0,0 +1,122 @@ +module ActiveRecord + module Tasks # :nodoc: + module DatabaseTasks # :nodoc: + extend self + + LOCAL_HOSTS = ['127.0.0.1', 'localhost'] + + def register_task(pattern, task) + @tasks ||= {} + @tasks[pattern] = task + end + + register_task(/mysql/, ActiveRecord::Tasks::MySQLDatabaseTasks) + register_task(/postgresql/, ActiveRecord::Tasks::PostgreSQLDatabaseTasks) + register_task(/sqlite/, ActiveRecord::Tasks::SQLiteDatabaseTasks) + + def create(*arguments) + configuration = arguments.first + class_for_adapter(configuration['adapter']).new(*arguments).create + rescue Exception => error + $stderr.puts error, *(error.backtrace) + $stderr.puts "Couldn't create database for #{configuration.inspect}" + end + + def create_all + each_local_configuration { |configuration| create configuration } + end + + def create_current(environment = Rails.env) + each_current_configuration(environment) { |configuration| + create configuration + } + ActiveRecord::Base.establish_connection environment + end + + def drop(*arguments) + configuration = arguments.first + class_for_adapter(configuration['adapter']).new(*arguments).drop + rescue Exception => error + $stderr.puts error, *(error.backtrace) + $stderr.puts "Couldn't drop #{configuration['database']}" + end + + def drop_all + each_local_configuration { |configuration| drop configuration } + end + + def drop_current(environment = Rails.env) + each_current_configuration(environment) { |configuration| + drop configuration + } + end + + def charset_current(environment = Rails.env) + charset ActiveRecord::Base.configurations[environment] + end + + def charset(*arguments) + configuration = arguments.first + class_for_adapter(configuration['adapter']).new(*arguments).charset + end + + def collation_current(environment = Rails.env) + collation ActiveRecord::Base.configurations[environment] + end + + def collation(*arguments) + configuration = arguments.first + class_for_adapter(configuration['adapter']).new(*arguments).collation + end + + def purge(configuration) + class_for_adapter(configuration['adapter']).new(configuration).purge + end + + def structure_dump(*arguments) + configuration = arguments.first + filename = arguments.delete_at 1 + class_for_adapter(configuration['adapter']).new(*arguments).structure_dump(filename) + end + + def structure_load(*arguments) + configuration = arguments.first + filename = arguments.delete_at 1 + class_for_adapter(configuration['adapter']).new(*arguments).structure_load(filename) + end + + private + + def class_for_adapter(adapter) + key = @tasks.keys.detect { |pattern| adapter[pattern] } + @tasks[key] + end + + def each_current_configuration(environment) + environments = [environment] + environments << 'test' if environment.development? + + configurations = ActiveRecord::Base.configurations.values_at(*environments) + configurations.compact.each do |configuration| + yield configuration unless configuration['database'].blank? + end + end + + def each_local_configuration + ActiveRecord::Base.configurations.each_value do |configuration| + next unless configuration['database'] + + if local_database?(configuration) + yield configuration + else + $stderr.puts "This task only modifies local databases. #{configuration['database']} is on a remote host." + end + end + end + + def local_database?(configuration) + configuration['host'].blank? || LOCAL_HOSTS.include?(configuration['host']) + end + end + end +end diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb new file mode 100644 index 0000000000..bf62dfd5b5 --- /dev/null +++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb @@ -0,0 +1,114 @@ +module ActiveRecord + module Tasks # :nodoc: + class MySQLDatabaseTasks # :nodoc: + + DEFAULT_CHARSET = ENV['CHARSET'] || 'utf8' + DEFAULT_COLLATION = ENV['COLLATION'] || 'utf8_unicode_ci' + ACCESS_DENIED_ERROR = 1045 + + delegate :connection, :establish_connection, to: ActiveRecord::Base + + def initialize(configuration) + @configuration = configuration + end + + def create + establish_connection configuration_without_database + connection.create_database configuration['database'], creation_options + establish_connection configuration + rescue error_class => error + raise error unless error.errno == ACCESS_DENIED_ERROR + + $stdout.print error.error + establish_connection root_configuration_without_database + connection.create_database configuration['database'], creation_options + connection.execute grant_statement.gsub(/\s+/, ' ').strip + establish_connection configuration + rescue error_class => error + $stderr.puts error.error + $stderr.puts "Couldn't create database for #{configuration.inspect}, #{creation_options.inspect}" + $stderr.puts "(If you set the charset manually, make sure you have a matching collation)" if configuration['charset'] + end + + def drop + establish_connection configuration + connection.drop_database configuration['database'] + end + + def purge + establish_connection :test + connection.recreate_database configuration['database'], creation_options + end + + def charset + connection.charset + end + + def collation + connection.collation + end + + def structure_dump(filename) + establish_connection configuration + File.open(filename, "w:utf-8") { |f| f << ActiveRecord::Base.connection.structure_dump } + end + + def structure_load(filename) + establish_connection(configuration) + connection.execute('SET foreign_key_checks = 0') + IO.read(filename).split("\n\n").each do |table| + connection.execute(table) + end + end + + private + + def configuration + @configuration + end + + def configuration_without_database + configuration.merge('database' => nil) + end + + def creation_options + { + charset: (configuration['charset'] || DEFAULT_CHARSET), + collation: (configuration['collation'] || DEFAULT_COLLATION) + } + end + + def error_class + case configuration['adapter'] + when /jdbc/ + require 'active_record/railties/jdbcmysql_error' + ArJdbcMySQL::Error + when /mysql2/ + Mysql2::Error + else + Mysql::Error + end + end + + def grant_statement + <<-SQL +GRANT ALL PRIVILEGES ON #{configuration['database']}.* + TO '#{configuration['username']}'@'localhost' +IDENTIFIED BY '#{configuration['password']}' WITH GRANT OPTION; + SQL + end + + def root_configuration_without_database + configuration_without_database.merge( + 'username' => 'root', + 'password' => root_password + ) + end + + def root_password + $stdout.print "Please provide the root password for your mysql installation\n>" + $stdin.gets.strip + end + end + end +end diff --git a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb new file mode 100644 index 0000000000..ea5cb888fb --- /dev/null +++ b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb @@ -0,0 +1,85 @@ +require 'shellwords' + +module ActiveRecord + module Tasks # :nodoc: + class PostgreSQLDatabaseTasks # :nodoc: + + DEFAULT_ENCODING = ENV['CHARSET'] || 'utf8' + + delegate :connection, :establish_connection, :clear_active_connections!, + to: ActiveRecord::Base + + def initialize(configuration) + @configuration = configuration + end + + def create(master_established = false) + establish_master_connection unless master_established + connection.create_database configuration['database'], + configuration.merge('encoding' => encoding) + establish_connection configuration + end + + def drop + establish_master_connection + connection.drop_database configuration['database'] + end + + def charset + connection.encoding + end + + def collation + connection.collation + end + + def purge + clear_active_connections! + drop + create true + end + + def structure_dump(filename) + set_psql_env + search_path = configuration['schema_search_path'] + unless search_path.blank? + search_path = search_path.split(",").map{|search_path_part| "--schema=#{Shellwords.escape(search_path_part.strip)}" }.join(" ") + end + + command = "pg_dump -i -s -x -O -f #{Shellwords.escape(filename)} #{search_path} #{Shellwords.escape(configuration['database'])}" + raise 'Error dumping database' unless Kernel.system(command) + + File.open(filename, "a") { |f| f << "SET search_path TO #{ActiveRecord::Base.connection.schema_search_path};\n\n" } + end + + def structure_load(filename) + set_psql_env + Kernel.system("psql -f #{filename} #{configuration['database']}") + end + + private + + def configuration + @configuration + end + + def encoding + configuration['encoding'] || DEFAULT_ENCODING + end + + def establish_master_connection + establish_connection configuration.merge( + 'database' => 'postgres', + 'schema_search_path' => 'public' + ) + end + + def set_psql_env + ENV['PGHOST'] = configuration['host'] if configuration['host'] + ENV['PGPORT'] = configuration['port'].to_s if configuration['port'] + ENV['PGPASSWORD'] = configuration['password'].to_s if configuration['password'] + ENV['PGUSER'] = configuration['username'].to_s if configuration['username'] + end + end + end +end diff --git a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb new file mode 100644 index 0000000000..da01058a82 --- /dev/null +++ b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb @@ -0,0 +1,55 @@ +module ActiveRecord + module Tasks # :nodoc: + class SQLiteDatabaseTasks # :nodoc: + + delegate :connection, :establish_connection, to: ActiveRecord::Base + + def initialize(configuration, root = Rails.root) + @configuration, @root = configuration, root + end + + def create + if File.exist?(configuration['database']) + $stderr.puts "#{configuration['database']} already exists" + return + end + + establish_connection configuration + connection + end + + def drop + require 'pathname' + path = Pathname.new configuration['database'] + file = path.absolute? ? path.to_s : File.join(root, path) + + FileUtils.rm(file) if File.exist?(file) + end + alias :purge :drop + + def charset + connection.encoding + end + + def structure_dump(filename) + dbfile = configuration['database'] + `sqlite3 #{dbfile} .schema > #{filename}` + end + + def structure_load(filename) + dbfile = configuration['database'] + `sqlite3 #{dbfile} < "#{filename}"` + end + + private + + def configuration + @configuration + end + + def root + @root + end + end + end +end diff --git a/activerecord/lib/active_record/test_case.rb b/activerecord/lib/active_record/test_case.rb index fcaa4b74a6..c035ad43a2 100644 --- a/activerecord/lib/active_record/test_case.rb +++ b/activerecord/lib/active_record/test_case.rb @@ -1,4 +1,3 @@ -require 'active_support/deprecation' require 'active_support/test_case' ActiveSupport::Deprecation.warn('ActiveRecord::TestCase is deprecated, please use ActiveSupport::TestCase') @@ -8,7 +7,7 @@ module ActiveRecord # Defines some test assertions to test against SQL queries. class TestCase < ActiveSupport::TestCase #:nodoc: def teardown - SQLCounter.log.clear + SQLCounter.clear_log end def assert_date_from_db(expected, actual, message = nil) @@ -22,47 +21,57 @@ module ActiveRecord end def assert_sql(*patterns_to_match) - SQLCounter.log = [] + SQLCounter.clear_log yield - SQLCounter.log + SQLCounter.log_all ensure failed_patterns = [] patterns_to_match.each do |pattern| - failed_patterns << pattern unless SQLCounter.log.any?{ |sql| pattern === sql } + failed_patterns << pattern unless SQLCounter.log_all.any?{ |sql| pattern === sql } end assert failed_patterns.empty?, "Query pattern(s) #{failed_patterns.map{ |p| p.inspect }.join(', ')} not found.#{SQLCounter.log.size == 0 ? '' : "\nQueries:\n#{SQLCounter.log.join("\n")}"}" end - def assert_queries(num = 1) - SQLCounter.log = [] + def assert_queries(num = 1, options = {}) + ignore_none = options.fetch(:ignore_none) { num == :any } + SQLCounter.clear_log yield ensure - assert_equal num, SQLCounter.log.size, "#{SQLCounter.log.size} instead of #{num} queries were executed.#{SQLCounter.log.size == 0 ? '' : "\nQueries:\n#{SQLCounter.log.join("\n")}"}" + the_log = ignore_none ? SQLCounter.log_all : SQLCounter.log + if num == :any + assert_operator the_log.size, :>=, 1, "1 or more queries expected, but none were executed." + else + mesg = "#{the_log.size} instead of #{num} queries were executed.#{the_log.size == 0 ? '' : "\nQueries:\n#{the_log.join("\n")}"}" + assert_equal num, the_log.size, mesg + end end def assert_no_queries(&block) - prev_ignored_sql = SQLCounter.ignored_sql - SQLCounter.ignored_sql = [] - assert_queries(0, &block) - ensure - SQLCounter.ignored_sql = prev_ignored_sql + assert_queries(0, :ignore_none => true, &block) end end class SQLCounter class << self - attr_accessor :ignored_sql, :log + attr_accessor :ignored_sql, :log, :log_all + def clear_log; self.log = []; self.log_all = []; end end - self.log = [] + self.clear_log 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, or better yet, use a different notification for the queries + # instead examining the SQL content. + oracle_ignored = [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im] + mysql_ignored = [/^SHOW TABLES/i, /^SHOW FULL FIELDS/] + postgresql_ignored = [/^\s*select\b.*\bfrom\b.*pg_namespace\b/im, /^\s*select\b.*\battname\b.*\bfrom\b.*\bpg_attribute\b/im] + + [oracle_ignored, mysql_ignored, postgresql_ignored].each do |db_ignored_sql| + ignored_sql.concat db_ignored_sql + end attr_reader :ignore @@ -75,8 +84,10 @@ module ActiveRecord # FIXME: this seems bad. we should probably have a better way to indicate # the query was cached - return if 'CACHE' == values[:name] || ignore =~ sql - self.class.log << sql + return if 'CACHE' == values[:name] + + self.class.log_all << sql + self.class.log << sql unless ignore =~ sql end end diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index c717fdea47..c32e0d6bf8 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -1,6 +1,10 @@ -require 'active_support/core_ext/class/attribute' module ActiveRecord + ActiveSupport.on_load(:active_record_config) do + mattr_accessor :record_timestamps, instance_accessor: false + self.record_timestamps = true + end + # = Active Record Timestamp # # Active Record automatically timestamps create and update operations if the @@ -33,8 +37,7 @@ module ActiveRecord extend ActiveSupport::Concern included do - config_attribute :record_timestamps, :instance_writer => true - self.record_timestamps = true + config_attribute :record_timestamps, instance_writer: true end def initialize_dup(other) diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 9cb9b4627b..9cec791faf 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -293,12 +293,12 @@ module ActiveRecord begin status = yield rescue ActiveRecord::Rollback - if defined?(@_start_transaction_state) + if defined?(@_start_transaction_state) @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 end status = nil end - + raise ActiveRecord::Rollback unless status end status @@ -308,7 +308,6 @@ module ActiveRecord # Save the new record state and id of a record so it can be restored later if a transaction fails. def remember_transaction_record_state #:nodoc: - @_start_transaction_state ||= {} @_start_transaction_state[:id] = id if has_attribute?(self.class.primary_key) @_start_transaction_state[:new_record] = @new_record @_start_transaction_state[:destroyed] = @destroyed @@ -317,18 +316,16 @@ module ActiveRecord # Clear the new record state and id of a record. def clear_transaction_record_state #:nodoc: - if defined?(@_start_transaction_state) - @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 - remove_instance_variable(:@_start_transaction_state) if @_start_transaction_state[:level] < 1 - end + @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 + @_start_transaction_state.clear if @_start_transaction_state[:level] < 1 end # Restore the new record state and id of a record that was previously saved by a call to save_record_state. def restore_transaction_record_state(force = false) #:nodoc: - if defined?(@_start_transaction_state) + unless @_start_transaction_state.empty? @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 - if @_start_transaction_state[:level] < 1 - restore_state = remove_instance_variable(:@_start_transaction_state) + if @_start_transaction_state[:level] < 1 || force + restore_state = @_start_transaction_state was_frozen = @attributes.frozen? @attributes = @attributes.dup if was_frozen @new_record = restore_state[:new_record] @@ -340,13 +337,14 @@ module ActiveRecord @attributes_cache.delete(self.class.primary_key) end @attributes.freeze if was_frozen + @_start_transaction_state.clear end end end # Determine if a record was created or destroyed in a transaction. State should be one of :new_record or :destroyed. def transaction_record_state(state) #:nodoc: - @_start_transaction_state[state] if defined?(@_start_transaction_state) + @_start_transaction_state[state] end # Determine if a transaction included an action for :create, :update, or :destroy. Used in filtering callbacks. diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index d06020b3ce..cef2bbd563 100644 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -81,3 +81,4 @@ end require "active_record/validations/associated" require "active_record/validations/uniqueness" +require "active_record/validations/presence" diff --git a/activerecord/lib/active_record/validations/associated.rb b/activerecord/lib/active_record/validations/associated.rb index afce149da9..1fa6629980 100644 --- a/activerecord/lib/active_record/validations/associated.rb +++ b/activerecord/lib/active_record/validations/associated.rb @@ -1,6 +1,6 @@ module ActiveRecord module Validations - class AssociatedValidator < ActiveModel::EachValidator + class AssociatedValidator < ActiveModel::EachValidator #:nodoc: def validate_each(record, attribute, value) if Array.wrap(value).reject {|r| r.marked_for_destruction? || r.valid?(record.validation_context) }.any? record.errors.add(attribute, :invalid, options.merge(:value => value)) @@ -9,7 +9,8 @@ module ActiveRecord end module ClassMethods - # Validates whether the associated object or objects are all valid themselves. Works with any kind of association. + # Validates whether the associated object or objects are all valid + # themselves. Works with any kind of association. # # class Book < ActiveRecord::Base # has_many :pages @@ -18,23 +19,28 @@ module ActiveRecord # validates_associated :pages, :library # end # - # WARNING: This validation must not be used on both ends of an association. Doing so will lead to a circular dependency and cause infinite recursion. + # WARNING: This validation must not be used on both ends of an association. + # Doing so will lead to a circular dependency and cause infinite recursion. # - # NOTE: This validation will not fail if the association hasn't been assigned. If you want to - # ensure that the association is both present and guaranteed to be valid, you also need to - # use +validates_presence_of+. + # NOTE: This validation will not fail if the association hasn't been + # assigned. If you want to ensure that the association is both present and + # guaranteed to be valid, you also need to use +validates_presence_of+. # # Configuration options: - # * <tt>:message</tt> - A custom error message (default is: "is invalid") + # + # * <tt>:message</tt> - A custom error message (default is: "is invalid"). # * <tt>:on</tt> - Specifies when this validation is active. Runs in all # 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 - # method, proc or string should return or evaluate to a true or false value. - # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should - # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The - # method, proc or string should return or evaluate to a true or false value. + # * <tt>:if</tt> - Specifies a method, proc or string to call to determine + # if the validation should occur (e.g. <tt>if: :allow_validation</tt>, + # or <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). The method, + # proc or string should return or evaluate to a +true+ or +false+ value. + # * <tt>:unless</tt> - Specifies a method, proc or string to call to + # determine if the validation should not occur (e.g. <tt>unless: :skip_validation</tt>, + # or <tt>unless: => Proc.new { |user| user.signup_step <= 2 }</tt>). The + # method, proc or string should return or evaluate to a +true+ or +false+ + # value. def validates_associated(*attr_names) validates_with AssociatedValidator, _merge_attributes(attr_names) end diff --git a/activerecord/lib/active_record/validations/presence.rb b/activerecord/lib/active_record/validations/presence.rb new file mode 100644 index 0000000000..056527b512 --- /dev/null +++ b/activerecord/lib/active_record/validations/presence.rb @@ -0,0 +1,64 @@ +module ActiveRecord + module Validations + class PresenceValidator < ActiveModel::Validations::PresenceValidator + def validate(record) + super + attributes.each do |attribute| + next unless record.class.reflect_on_association(attribute) + value = record.send(attribute) + if Array(value).all? { |r| r.marked_for_destruction? } + record.errors.add(attribute, :blank, options) + end + end + end + end + + module ClassMethods + # Validates that the specified attributes are not blank (as defined by + # Object#blank?), and, if the attribute is an association, that the + # associated object is not marked for destruction. Happens by default + # on save. + # + # class Person < ActiveRecord::Base + # has_one :face + # validates_presence_of :face + # end + # + # The face attribute must be in the object and it cannot be blank or marked + # for destruction. + # + # If you want to validate the presence of a boolean field (where the real values + # are true and false), you will want to use + # <tt>validates_inclusion_of :field_name, :in => [true, false]</tt>. + # + # This is due to the way Object#blank? handles boolean values: + # <tt>false.blank? # => true</tt>. + # + # This validator defers to the ActiveModel validation for presence, adding the + # check to see that an associated object is not marked for destruction. This + # prevents the parent object from validating successfully and saving, which then + # deletes the associated object, thus putting the parent object into an invalid + # state. + # + # Configuration options: + # * <tt>:message</tt> - A custom error message (default is: "can't be blank"). + # * <tt>:on</tt> - Specifies when this validation is active. Runs in all + # 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 method, proc + # or string should return or evaluate to a true or false value. + # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine + # if the validation should not occur (e.g. <tt>:unless => :skip_validation</tt>, + # or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The method, + # proc or string should return or evaluate to a true or false value. + # * <tt>:strict</tt> - Specifies whether validation should be strict. + # See <tt>ActiveModel::Validation#validates!</tt> for more information. + # + def validates_presence_of(*attr_names) + validates_with PresenceValidator, _merge_attributes(attr_names) + end + end + end +end diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 9e4b588ac2..c117872ac8 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -2,7 +2,7 @@ require 'active_support/core_ext/array/prepend_and_append' module ActiveRecord module Validations - class UniquenessValidator < ActiveModel::EachValidator + class UniquenessValidator < ActiveModel::EachValidator #:nodoc: def initialize(options) super(options.reverse_merge(:case_sensitive => true)) end @@ -26,7 +26,7 @@ module ActiveRecord relation = relation.and(table[finder_class.primary_key.to_sym].not_eq(record.send(:id))) if record.persisted? Array(options[:scope]).each do |scope_item| - scope_value = record.send(scope_item) + scope_value = record.read_attribute(scope_item) reflection = record.class.reflect_on_association(scope_item) if reflection scope_value = record.send(reflection.foreign_key) @@ -87,54 +87,67 @@ module ActiveRecord end module ClassMethods - # Validates whether the value of the specified attributes are unique across the system. - # Useful for making sure that only one user + # Validates whether the value of the specified attributes are unique + # across the system. Useful for making sure that only one user # can be named "davidhh". # # class Person < ActiveRecord::Base # validates_uniqueness_of :user_name # end # - # It can also validate whether the value of the specified attributes are unique based on a scope parameter: + # It can also validate whether the value of the specified attributes are + # unique based on a <tt>:scope</tt> parameter: # # class Person < ActiveRecord::Base - # validates_uniqueness_of :user_name, :scope => :account_id + # 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 - # per semester for a particular class. + # 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 - # validates_uniqueness_of :teacher_id, :scope => [:semester_id, :class_id] + # validates_uniqueness_of :teacher_id, scope: [:semester_id, :class_id] # end # - # It is also possible to limit the uniqueness constraint to a set of records matching certain conditions. - # In this example archived articles are not being taken into consideration when validating uniqueness + # It is also possible to limit the uniqueness constraint to a set of + # records matching certain conditions. In this example archived articles + # are not being taken into consideration when validating uniqueness # of the title attribute: # # class Article < ActiveRecord::Base - # validates_uniqueness_of :title, :conditions => where('status != ?', 'archived') + # validates_uniqueness_of :title, conditions: where('status != ?', 'archived') # end # - # When the record is created, a check is performed to make sure that no record exists in the database - # with the given value for the specified attribute (that maps to a column). When the record is updated, + # When the record is created, a check is performed to make sure that no + # record exists in the database with the given value for the specified + # attribute (that maps to a column). When the record is updated, # the same check is made but disregarding the record itself. # # Configuration options: - # * <tt>:message</tt> - Specifies a custom error message (default is: "has already been taken"). - # * <tt>:scope</tt> - One or more columns by which to limit the scope of the uniqueness constraint. - # * <tt>:conditions</tt> - Specify the conditions to be included as a <tt>WHERE</tt> SQL fragment to limit - # the uniqueness constraint lookup. (e.g. <tt>:conditions => where('status = ?', 'active')</tt>) - # * <tt>:case_sensitive</tt> - Looks for an exact match. Ignored by non-text columns (+true+ by default). - # * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute is +nil+ (default is +false+). - # * <tt>:allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is +false+). - # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should - # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). - # The method, proc or string should return or evaluate to a true or false value. - # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should - # not occur (e.g. <tt>:unless => :skip_validation</tt>, or - # <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The method, proc or string should - # return or evaluate to a true or false value. + # + # * <tt>:message</tt> - Specifies a custom error message (default is: + # "has already been taken"). + # * <tt>:scope</tt> - One or more columns by which to limit the scope of + # the uniqueness constraint. + # * <tt>:conditions</tt> - Specify the conditions to be included as a + # <tt>WHERE</tt> SQL fragment to limit the uniqueness constraint lookup + # (e.g. <tt>conditions: where('status = ?', 'active')</tt>). + # * <tt>:case_sensitive</tt> - Looks for an exact match. Ignored by + # non-text columns (+true+ by default). + # * <tt>:allow_nil</tt> - If set to +true+, skips this validation if the + # attribute is +nil+ (default is +false+). + # * <tt>:allow_blank</tt> - If set to +true+, skips this validation if the + # attribute is blank (default is +false+). + # * <tt>:if</tt> - Specifies a method, proc or string to call to determine + # if the validation should occur (e.g. <tt>if: :allow_validation</tt>, + # or <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). The method, + # proc or string should return or evaluate to a +true+ or +false+ value. + # * <tt>:unless</tt> - Specifies a method, proc or string to call to + # determine if the validation should ot 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. # # === Concurrency and integrity # @@ -190,15 +203,16 @@ module ActiveRecord # # The bundled ActiveRecord::ConnectionAdapters distinguish unique index # constraint errors from other types of database errors by throwing an - # ActiveRecord::RecordNotUnique exception. - # For other adapters you will have to parse the (database-specific) exception - # message to detect such a case. + # ActiveRecord::RecordNotUnique exception. For other adapters you will + # have to parse the (database-specific) exception message to detect such + # a case. + # # The following bundled adapters throw the ActiveRecord::RecordNotUnique exception: + # # * ActiveRecord::ConnectionAdapters::MysqlAdapter # * ActiveRecord::ConnectionAdapters::Mysql2Adapter # * ActiveRecord::ConnectionAdapters::SQLite3Adapter # * ActiveRecord::ConnectionAdapters::PostgreSQLAdapter - # def validates_uniqueness_of(*attr_names) validates_with UniquenessValidator, _merge_attributes(attr_names) end diff --git a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb index 1509e34473..f6a432c6e5 100644 --- a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb +++ b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb @@ -11,15 +11,36 @@ module ActiveRecord end protected - attr_reader :migration_action + attr_reader :migration_action, :join_tables - def set_local_assigns! - if file_name =~ /^(add|remove)_.*_(?:to|from)_(.*)/ - @migration_action = $1 - @table_name = $2.pluralize + def set_local_assigns! + case file_name + when /^(add|remove)_.*_(?:to|from)_(.*)/ + @migration_action = $1 + @table_name = $2.pluralize + when /join_table/ + if attributes.length == 2 + @migration_action = 'join' + @join_tables = attributes.map(&:plural_name) + + set_index_names end end + end + + def set_index_names + attributes.each_with_index do |attr, i| + attr.index_name = [attr, attributes[i - 1]].map{ |a| index_name_for(a) } + end + end + def index_name_for(attribute) + if attribute.foreign_key? + attribute.name + else + attribute.name.singularize.foreign_key + end.to_sym + end end end end diff --git a/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb b/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb index b1a0f83b5f..d5c07aecd3 100644 --- a/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb +++ b/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb @@ -2,30 +2,50 @@ class <%= migration_class_name %> < ActiveRecord::Migration <%- if migration_action == 'add' -%> def change <% attributes.each do |attribute| -%> + <%- if attribute.reference? -%> + add_reference :<%= table_name %>, :<%= attribute.name %><%= attribute.inject_options %> + <%- else -%> add_column :<%= table_name %>, :<%= attribute.name %>, :<%= attribute.type %><%= attribute.inject_options %> <%- if attribute.has_index? -%> add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %> <%- end -%> + <%- end -%> <%- end -%> end +<%- elsif migration_action == 'join' -%> + def change + create_join_table :<%= join_tables.first %>, :<%= join_tables.second %> do |t| + <%- attributes.each do |attribute| -%> + <%= '# ' unless attribute.has_index? -%>t.index <%= attribute.index_name %><%= attribute.inject_index_options %> + <%- end -%> + end + end <%- else -%> def up <% attributes.each do |attribute| -%> - <%- if migration_action -%> - <%= migration_action %>_column :<%= table_name %>, :<%= attribute.name %> +<%- if migration_action -%> + <%- if attribute.reference? -%> + remove_reference :<%= table_name %>, :<%= attribute.name %><%= ', polymorphic: true' if attribute.polymorphic? %> + <%- else -%> + remove_column :<%= table_name %>, :<%= attribute.name %> <%- end -%> <%- end -%> +<%- end -%> end def down <% attributes.reverse.each do |attribute| -%> - <%- if migration_action -%> +<%- if migration_action -%> + <%- if attribute.reference? -%> + add_reference :<%= table_name %>, :<%= attribute.name %><%= attribute.inject_options %> + <%- else -%> add_column :<%= table_name %>, :<%= attribute.name %>, :<%= attribute.type %><%= attribute.inject_options %> <%- if attribute.has_index? -%> add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %> <%- end -%> <%- end -%> <%- end -%> +<%- end -%> end <%- end -%> end diff --git a/activerecord/lib/rails/generators/active_record/model/templates/model.rb b/activerecord/lib/rails/generators/active_record/model/templates/model.rb index d56f9f57a4..2cca17b94f 100644 --- a/activerecord/lib/rails/generators/active_record/model/templates/model.rb +++ b/activerecord/lib/rails/generators/active_record/model/templates/model.rb @@ -1,7 +1,7 @@ <% module_namespacing do -%> class <%= class_name %> < <%= parent_class_name.classify %> <% attributes.select {|attr| attr.reference? }.each do |attribute| -%> - belongs_to :<%= attribute.name %> + belongs_to :<%= attribute.name %><%= ', polymorphic: true' if attribute.polymorphic? %> <% end -%> <% if !accessible_attributes.empty? -%> attr_accessible <%= accessible_attributes.map {|a| ":#{a.name}" }.sort.join(', ') %> diff --git a/activerecord/lib/rails/generators/active_record/model/templates/module.rb b/activerecord/lib/rails/generators/active_record/model/templates/module.rb index fca2908080..a3bf1c37b6 100644 --- a/activerecord/lib/rails/generators/active_record/model/templates/module.rb +++ b/activerecord/lib/rails/generators/active_record/model/templates/module.rb @@ -1,7 +1,7 @@ <% module_namespacing do -%> module <%= class_path.map(&:camelize).join('::') %> def self.table_name_prefix - '<%= class_path.join('_') %>_' + '<%= namespaced? ? namespaced_class_path.join('_') : class_path.join('_') %>_' end end <% end -%> diff --git a/activerecord/lib/rails/generators/active_record/session_migration/session_migration_generator.rb b/activerecord/lib/rails/generators/active_record/session_migration/session_migration_generator.rb index 90923f6e74..75aee4f408 100644 --- a/activerecord/lib/rails/generators/active_record/session_migration/session_migration_generator.rb +++ b/activerecord/lib/rails/generators/active_record/session_migration/session_migration_generator.rb @@ -1,5 +1,4 @@ require 'rails/generators/active_record' -require 'active_support/core_ext/object/inclusion' module ActiveRecord module Generators @@ -14,8 +13,8 @@ module ActiveRecord def session_table_name current_table_name = ActiveRecord::SessionStore::Session.table_name - if current_table_name.in?(["sessions", "session"]) - current_table_name = (ActiveRecord::Base.pluralize_table_names ? 'session'.pluralize : 'session') + if current_table_name == 'session' || current_table_name == 'sessions' + current_table_name = ActiveRecord::Base.pluralize_table_names ? 'sessions' : 'session' end current_table_name end diff --git a/activerecord/test/active_record/connection_adapters/fake_adapter.rb b/activerecord/test/active_record/connection_adapters/fake_adapter.rb index 69dfd2503e..1199be68eb 100644 --- a/activerecord/test/active_record/connection_adapters/fake_adapter.rb +++ b/activerecord/test/active_record/connection_adapters/fake_adapter.rb @@ -1,6 +1,6 @@ module ActiveRecord - class Base - def self.fake_connection(config) + module ConnectionHandling + def fake_connection(config) ConnectionAdapters::FakeAdapter.new nil, logger end end diff --git a/activerecord/test/cases/adapters/mysql/connection_test.rb b/activerecord/test/cases/adapters/mysql/connection_test.rb index 5e1c52c9ba..4bccd2cc59 100644 --- a/activerecord/test/cases/adapters/mysql/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql/connection_test.rb @@ -70,11 +70,14 @@ class MysqlConnectionTest < ActiveRecord::TestCase assert_equal %w{ id data }, result.columns @connection.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")') + + # if there are no bind parameters, it will return a string (due to + # the libmysql api) result = @connection.exec_query('SELECT id, data FROM ex') assert_equal 1, result.rows.length assert_equal 2, result.columns.length - assert_equal [[1, 'foo']], result.rows + assert_equal [['1', 'foo']], result.rows end def test_exec_with_binds @@ -125,11 +128,12 @@ class MysqlConnectionTest < ActiveRecord::TestCase assert_equal [["STRICT_ALL_TABLES"]], result.rows end - def test_mysql_strict_mode_disabled + def test_mysql_strict_mode_disabled_dont_override_global_sql_mode run_without_connection do |orig_connection| ActiveRecord::Model.establish_connection(orig_connection.merge({:strict => false})) - result = ActiveRecord::Model.connection.exec_query "SELECT @@SESSION.sql_mode" - assert_equal [['']], result.rows + global_sql_mode = ActiveRecord::Model.connection.exec_query "SELECT @@GLOBAL.sql_mode" + session_sql_mode = ActiveRecord::Model.connection.exec_query "SELECT @@SESSION.sql_mode" + assert_equal global_sql_mode.rows, session_sql_mode.rows end end diff --git a/activerecord/test/cases/adapters/mysql/enum_test.rb b/activerecord/test/cases/adapters/mysql/enum_test.rb new file mode 100644 index 0000000000..40af317ad1 --- /dev/null +++ b/activerecord/test/cases/adapters/mysql/enum_test.rb @@ -0,0 +1,10 @@ +require "cases/helper" + +class MysqlEnumTest < ActiveRecord::TestCase + class EnumTest < ActiveRecord::Base + end + + def test_enum_limit + assert_equal 5, EnumTest.columns.first.limit + end +end diff --git a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb index 475a292f85..ddfe42b375 100644 --- a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb +++ b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb @@ -26,7 +26,9 @@ module ActiveRecord result = @conn.exec_query('SELECT number FROM ex WHERE number = 10') assert_equal 1, result.rows.length - assert_equal 10, result.rows.last.last + # if there are no bind parameters, it will return a string (due to + # the libmysql api) + assert_equal '10', result.rows.last.last end def test_exec_insert_string diff --git a/activerecord/test/cases/adapters/mysql/reserved_word_test.rb b/activerecord/test/cases/adapters/mysql/reserved_word_test.rb index 6faceaf7c0..aff971a955 100644 --- a/activerecord/test/cases/adapters/mysql/reserved_word_test.rb +++ b/activerecord/test/cases/adapters/mysql/reserved_word_test.rb @@ -34,11 +34,11 @@ class MysqlReservedWordTest < ActiveRecord::TestCase 'select'=>'id int auto_increment primary key', 'values'=>'id int auto_increment primary key, group_id int', 'distinct'=>'id int auto_increment primary key', - 'distincts_selects'=>'distinct_id int, select_id int' + 'distinct_select'=>'distinct_id int, select_id int' end def teardown - drop_tables_directly ['group', 'select', 'values', 'distinct', 'distincts_selects', 'order'] + drop_tables_directly ['group', 'select', 'values', 'distinct', 'distinct_select', 'order'] end # create tables with reserved-word names and columns @@ -80,7 +80,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase #activerecord model class with reserved-word table name def test_activerecord_model - create_test_fixtures :select, :distinct, :group, :values, :distincts_selects + create_test_fixtures :select, :distinct, :group, :values, :distinct_select x = nil assert_nothing_raised { x = Group.new } x.order = 'x' @@ -94,7 +94,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase # has_one association with reserved-word table name def test_has_one_associations - create_test_fixtures :select, :distinct, :group, :values, :distincts_selects + create_test_fixtures :select, :distinct, :group, :values, :distinct_select v = nil assert_nothing_raised { v = Group.find(1).values } assert_equal 2, v.id @@ -102,7 +102,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase # belongs_to association with reserved-word table name def test_belongs_to_associations - create_test_fixtures :select, :distinct, :group, :values, :distincts_selects + create_test_fixtures :select, :distinct, :group, :values, :distinct_select gs = nil assert_nothing_raised { gs = Select.find(2).groups } assert_equal gs.length, 2 @@ -111,7 +111,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase # has_and_belongs_to_many with reserved-word table name def test_has_and_belongs_to_many - create_test_fixtures :select, :distinct, :group, :values, :distincts_selects + create_test_fixtures :select, :distinct, :group, :values, :distinct_select s = nil assert_nothing_raised { s = Distinct.find(1).selects } assert_equal s.length, 2 @@ -130,7 +130,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase end def test_associations_work_with_reserved_words - assert_nothing_raised { Select.scoped(:includes => [:groups]).all } + assert_nothing_raised { Select.all.merge!(:includes => [:groups]).to_a } end #the following functions were added to DRY test cases diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb index 276c499276..c63e4fe5b6 100644 --- a/activerecord/test/cases/adapters/mysql2/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb @@ -44,11 +44,12 @@ class MysqlConnectionTest < ActiveRecord::TestCase assert_equal [["STRICT_ALL_TABLES"]], result.rows end - def test_mysql_strict_mode_disabled + def test_mysql_strict_mode_disabled_dont_override_global_sql_mode run_without_connection do |orig_connection| ActiveRecord::Model.establish_connection(orig_connection.merge({:strict => false})) - result = ActiveRecord::Model.connection.exec_query "SELECT @@SESSION.sql_mode" - assert_equal [['']], result.rows + global_sql_mode = ActiveRecord::Model.connection.exec_query "SELECT @@GLOBAL.sql_mode" + session_sql_mode = ActiveRecord::Model.connection.exec_query "SELECT @@SESSION.sql_mode" + assert_equal global_sql_mode.rows, session_sql_mode.rows end end diff --git a/activerecord/test/cases/adapters/mysql2/enum_test.rb b/activerecord/test/cases/adapters/mysql2/enum_test.rb new file mode 100644 index 0000000000..f3a05e48ad --- /dev/null +++ b/activerecord/test/cases/adapters/mysql2/enum_test.rb @@ -0,0 +1,10 @@ +require "cases/helper" + +class Mysql2EnumTest < ActiveRecord::TestCase + class EnumTest < ActiveRecord::Base + end + + def test_enum_limit + assert_equal 5, EnumTest.columns.first.limit + 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 32d4282623..9fd07f014e 100644 --- a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb +++ b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb @@ -34,11 +34,11 @@ class MysqlReservedWordTest < ActiveRecord::TestCase 'select'=>'id int auto_increment primary key', 'values'=>'id int auto_increment primary key, group_id int', 'distinct'=>'id int auto_increment primary key', - 'distincts_selects'=>'distinct_id int, select_id int' + 'distinct_select'=>'distinct_id int, select_id int' end def teardown - drop_tables_directly ['group', 'select', 'values', 'distinct', 'distincts_selects', 'order'] + drop_tables_directly ['group', 'select', 'values', 'distinct', 'distinct_select', 'order'] end # create tables with reserved-word names and columns @@ -80,7 +80,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase #activerecord model class with reserved-word table name def test_activerecord_model - create_test_fixtures :select, :distinct, :group, :values, :distincts_selects + create_test_fixtures :select, :distinct, :group, :values, :distinct_select x = nil assert_nothing_raised { x = Group.new } x.order = 'x' @@ -94,7 +94,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase # has_one association with reserved-word table name def test_has_one_associations - create_test_fixtures :select, :distinct, :group, :values, :distincts_selects + create_test_fixtures :select, :distinct, :group, :values, :distinct_select v = nil assert_nothing_raised { v = Group.find(1).values } assert_equal 2, v.id @@ -102,7 +102,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase # belongs_to association with reserved-word table name def test_belongs_to_associations - create_test_fixtures :select, :distinct, :group, :values, :distincts_selects + create_test_fixtures :select, :distinct, :group, :values, :distinct_select gs = nil assert_nothing_raised { gs = Select.find(2).groups } assert_equal gs.length, 2 @@ -111,7 +111,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase # has_and_belongs_to_many with reserved-word table name def test_has_and_belongs_to_many - create_test_fixtures :select, :distinct, :group, :values, :distincts_selects + create_test_fixtures :select, :distinct, :group, :values, :distinct_select s = nil assert_nothing_raised { s = Distinct.find(1).selects } assert_equal s.length, 2 @@ -130,7 +130,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase end def test_associations_work_with_reserved_words - assert_nothing_raised { Select.scoped(:includes => [:groups]).all } + assert_nothing_raised { Select.all.merge!(:includes => [:groups]).to_a } end #the following functions were added to DRY test cases diff --git a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb index 447d729e52..113c27b194 100644 --- a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb @@ -21,6 +21,10 @@ class PostgresqlActiveSchemaTest < ActiveRecord::TestCase assert_equal %(CREATE DATABASE "aimonetti" ENCODING = 'latin1'), create_database(:aimonetti, :encoding => :latin1) end + def test_create_database_with_collation_and_ctype + assert_equal %(CREATE DATABASE "aimonetti" ENCODING = 'UTF8' LC_COLLATE = 'ja_JP.UTF8' LC_CTYPE = 'ja_JP.UTF8'), create_database(:aimonetti, :encoding => :"UTF8", :collation => :"ja_JP.UTF8", :ctype => :"ja_JP.UTF8") + end + def test_add_index # add_index calls index_name_exists? which can't work since execute is stubbed ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send(:define_method, :index_name_exists?) do |*| diff --git a/activerecord/test/cases/adapters/postgresql/connection_test.rb b/activerecord/test/cases/adapters/postgresql/connection_test.rb index adb2cef010..1ff307c735 100644 --- a/activerecord/test/cases/adapters/postgresql/connection_test.rb +++ b/activerecord/test/cases/adapters/postgresql/connection_test.rb @@ -21,6 +21,18 @@ module ActiveRecord assert_not_nil @connection.encoding end + def test_collation + assert_not_nil @connection.collation + end + + def test_ctype + assert_not_nil @connection.ctype + end + + def test_default_client_min_messages + assert_equal "warning", @connection.client_min_messages + end + # Ensure, we can set connection params using the example of Generic # Query Optimizer (geqo). It is 'on' per default. def test_connection_options @@ -69,5 +81,78 @@ module ActiveRecord assert_equal 'SCHEMA', @connection.logged[0][1] end + def test_reconnection_after_simulated_disconnection_with_verify + assert @connection.active? + original_connection_pid = @connection.query('select pg_backend_pid()') + + # Fail with bad connection on next query attempt. + raw_connection = @connection.raw_connection + raw_connection_class = class << raw_connection ; self ; end + raw_connection_class.class_eval <<-CODE, __FILE__, __LINE__ + 1 + def query_fake(*args) + if !( @called ||= false ) + self.stubs(:status).returns(PGconn::CONNECTION_BAD) + @called = true + raise PGError + else + self.unstub(:status) + query_unfake(*args) + end + end + + alias query_unfake query + alias query query_fake + CODE + + begin + @connection.verify! + new_connection_pid = @connection.query('select pg_backend_pid()') + ensure + raw_connection_class.class_eval <<-CODE + alias query query_unfake + undef query_fake + CODE + end + + assert_not_equal original_connection_pid, new_connection_pid, "Should have a new underlying connection pid" + end + + # Must have with_manual_interventions set to true for this + # test to run. + # When prompted, restart the PostgreSQL server with the + # "-m fast" option or kill the individual connection assuming + # you know the incantation to do that. + # To restart PostgreSQL 9.1 on OS X, installed via MacPorts, ... + # sudo su postgres -c "pg_ctl restart -D /opt/local/var/db/postgresql91/defaultdb/ -m fast" + def test_reconnection_after_actual_disconnection_with_verify + skip "with_manual_interventions is false in configuration" unless ARTest.config['with_manual_interventions'] + + original_connection_pid = @connection.query('select pg_backend_pid()') + + # Sanity check. + assert @connection.active? + + puts 'Kill the connection now (e.g. by restarting the PostgreSQL ' + + 'server with the "-m fast" option) and then press enter.' + $stdin.gets + + @connection.verify! + + assert @connection.active? + + # If we get no exception here, then either we re-connected successfully, or + # we never actually got disconnected. + new_connection_pid = @connection.query('select pg_backend_pid()') + + assert_not_equal original_connection_pid, new_connection_pid, + "umm -- looks like you didn't break the connection, because we're still " + + "successfully querying with the same connection pid." + + # Repair all fixture connections so other tests won't break. + @fixture_connections.each do |c| + c.verify! + end + end + end end diff --git a/activerecord/test/cases/adapters/postgresql/datatype_test.rb b/activerecord/test/cases/adapters/postgresql/datatype_test.rb index 34660577da..a4d9286d52 100644 --- a/activerecord/test/cases/adapters/postgresql/datatype_test.rb +++ b/activerecord/test/cases/adapters/postgresql/datatype_test.rb @@ -27,6 +27,9 @@ end class PostgresqlTimestampWithZone < ActiveRecord::Base end +class PostgresqlUUID < ActiveRecord::Base +end + class PostgresqlDataTypeTest < ActiveRecord::TestCase self.use_transactional_fixtures = false @@ -61,6 +64,9 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase @first_oid = PostgresqlOid.find(1) @connection.execute("INSERT INTO postgresql_timestamp_with_zones (time) VALUES ('2010-01-01 10:00:00-1')") + + @connection.execute("INSERT INTO postgresql_uuids (guid, compact_guid) VALUES('d96c3da0-96c1-012f-1316-64ce8f32c6d8', 'f06c715096c1012f131764ce8f32c6d8')") + @first_uuid = PostgresqlUUID.find(1) end def test_data_type_of_array_types @@ -100,6 +106,10 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase assert_equal :integer, @first_oid.column_for_attribute(:obj_id).type end + def test_data_type_of_uuid_types + assert_equal :uuid, @first_uuid.column_for_attribute(:guid).type + end + def test_array_values assert_equal '{35000,21000,18000,17000}', @first_array.commission_by_quarter assert_equal '{foo,bar,baz}', @first_array.nicknames @@ -143,6 +153,11 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase assert_equal '01:23:45:67:89:0a', @first_network_address.mac_address end + def test_uuid_values + assert_equal 'd96c3da0-96c1-012f-1316-64ce8f32c6d8', @first_uuid.guid + assert_equal 'f06c7150-96c1-012f-1317-64ce8f32c6d8', @first_uuid.compact_guid + end + def test_bit_string_values assert_equal '00010101', @first_bit_string.bit_string assert_equal '00010101', @first_bit_string.bit_string_varying diff --git a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb index 337f43c421..26507ad654 100644 --- a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb +++ b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb @@ -27,4 +27,69 @@ class TimestampTest < ActiveRecord::TestCase d = Developer.create!(:name => 'aaron', :updated_at => -1.0 / 0.0) assert_equal(-1.0 / 0.0, d.updated_at) end + + def test_default_datetime_precision + ActiveRecord::Base.connection.create_table(:foos) + ActiveRecord::Base.connection.add_column :foos, :created_at, :datetime + ActiveRecord::Base.connection.add_column :foos, :updated_at, :datetime + assert_nil activerecord_column_option('foos', 'created_at', 'precision') + end + + def test_timestamp_data_type_with_precision + ActiveRecord::Base.connection.create_table(:foos) + ActiveRecord::Base.connection.add_column :foos, :created_at, :datetime, :precision => 0 + ActiveRecord::Base.connection.add_column :foos, :updated_at, :datetime, :precision => 5 + assert_equal 0, activerecord_column_option('foos', 'created_at', 'precision') + assert_equal 5, activerecord_column_option('foos', 'updated_at', 'precision') + end + + def test_timestamps_helper_with_custom_precision + ActiveRecord::Base.connection.create_table(:foos) do |t| + t.timestamps :precision => 4 + end + assert_equal 4, activerecord_column_option('foos', 'created_at', 'precision') + assert_equal 4, activerecord_column_option('foos', 'updated_at', 'precision') + end + + def test_passing_precision_to_timestamp_does_not_set_limit + ActiveRecord::Base.connection.create_table(:foos) do |t| + t.timestamps :precision => 4 + end + assert_nil activerecord_column_option("foos", "created_at", "limit") + assert_nil activerecord_column_option("foos", "updated_at", "limit") + end + + def test_invalid_timestamp_precision_raises_error + assert_raises ActiveRecord::ActiveRecordError do + ActiveRecord::Base.connection.create_table(:foos) do |t| + t.timestamps :precision => 7 + end + end + end + + def test_postgres_agrees_with_activerecord_about_precision + ActiveRecord::Base.connection.create_table(:foos) do |t| + t.timestamps :precision => 4 + end + assert_equal '4', pg_datetime_precision('foos', 'created_at') + assert_equal '4', pg_datetime_precision('foos', 'updated_at') + end + + private + + def pg_datetime_precision(table_name, column_name) + results = ActiveRecord::Base.connection.execute("SELECT column_name, datetime_precision FROM information_schema.columns WHERE table_name ='#{table_name}'") + result = results.find do |result_hash| + result_hash["column_name"] == column_name + end + result && result["datetime_precision"] + end + + def activerecord_column_option(tablename, column_name, option) + result = ActiveRecord::Base.connection.columns(tablename).find do |column| + column.name == column_name + end + result && result.send(option) + end + end diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb index 5e947799cc..4e26c5dda1 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -24,7 +24,7 @@ module ActiveRecord @conn.extend(LogIntercepter) @conn.intercepted = true end - + def teardown @conn.intercepted = false @conn.logged = [] @@ -43,11 +43,6 @@ module ActiveRecord assert(!result.rows.first.include?("blob"), "should not store blobs") end - def test_time_column - owner = Owner.create!(:eats_at => Time.utc(1995,1,1,6,0)) - assert_match(/1995-01-01/, owner.reload.eats_at.to_s) - end - def test_exec_insert column = @conn.columns('items').find { |col| col.name == 'number' } vals = [[column, 10]] diff --git a/activerecord/test/cases/associations/association_scope_test.rb b/activerecord/test/cases/associations/association_scope_test.rb new file mode 100644 index 0000000000..d38648202e --- /dev/null +++ b/activerecord/test/cases/associations/association_scope_test.rb @@ -0,0 +1,15 @@ +require 'cases/helper' +require 'models/post' +require 'models/author' + +module ActiveRecord + module Associations + class AssociationScopeTest < ActiveRecord::TestCase + test 'does not duplicate conditions' do + association_scope = AssociationScope.new(Author.new.association(:welcome_posts)) + wheres = association_scope.scope.where_values.map(&:right) + assert_equal wheres.uniq, wheres + end + 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 2c7a240915..5f7825783b 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -73,14 +73,14 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase def test_eager_loading_with_primary_key Firm.create("name" => "Apple") Client.create("name" => "Citibank", :firm_name => "Apple") - citibank_result = Client.scoped(:where => {:name => "Citibank"}, :includes => :firm_with_primary_key).first + citibank_result = Client.all.merge!(:where => {:name => "Citibank"}, :includes => :firm_with_primary_key).first assert citibank_result.association_cache.key?(:firm_with_primary_key) end def test_eager_loading_with_primary_key_as_symbol Firm.create("name" => "Apple") Client.create("name" => "Citibank", :firm_name => "Apple") - citibank_result = Client.scoped(:where => {:name => "Citibank"}, :includes => :firm_with_primary_key_symbols).first + citibank_result = Client.all.merge!(:where => {:name => "Citibank"}, :includes => :firm_with_primary_key_symbols).first assert citibank_result.association_cache.key?(:firm_with_primary_key_symbols) end @@ -181,8 +181,8 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase end def test_with_select - assert_equal Company.find(2).firm_with_select.attributes.size, 1 - assert_equal Company.scoped(:includes => :firm_with_select ).find(2).firm_with_select.attributes.size, 1 + assert_equal 1, Company.find(2).firm_with_select.attributes.size + assert_equal 1, Company.all.merge!(:includes => :firm_with_select ).find(2).firm_with_select.attributes.size end def test_belongs_to_counter @@ -298,12 +298,12 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_equal 1, Topic.find(topic.id)[:replies_count] end - def test_belongs_to_counter_when_update_column + def test_belongs_to_counter_when_update_columns 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") + topic.update_columns(content: "rails is wonderfull") assert_equal 1, Topic.find(topic.id)[:replies_count] end @@ -334,7 +334,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase def test_new_record_with_foreign_key_but_no_object c = Client.new("firm_id" => 1) # sometimes tests on Oracle fail if ORDER BY is not provided therefore add always :order with :first - assert_equal Firm.scoped(:order => "id").first, c.firm_with_basic_id + assert_equal Firm.all.merge!(:order => "id").first, c.firm_with_basic_id end def test_setting_foreign_key_after_nil_target_loaded @@ -396,7 +396,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase def test_association_assignment_sticks post = Post.first - author1, author2 = Author.scoped(:limit => 2).all + author1, author2 = Author.all.merge!(:limit => 2).to_a assert_not_nil author1 assert_not_nil author2 @@ -498,14 +498,14 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_nothing_raised do Account.find(@account.id).save! - Account.scoped(:includes => :firm).find(@account.id).save! + Account.all.merge!(:includes => :firm).find(@account.id).save! end @account.firm.delete assert_nothing_raised do Account.find(@account.id).save! - Account.scoped(:includes => :firm).find(@account.id).save! + Account.all.merge!(:includes => :firm).find(@account.id).save! end end @@ -524,13 +524,13 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase def test_invalid_belongs_to_dependent_option_nullify_raises_exception assert_raise ArgumentError do - Author.belongs_to :special_author_address, :dependent => :nullify + Class.new(Author).belongs_to :special_author_address, :dependent => :nullify end end def test_invalid_belongs_to_dependent_option_restrict_raises_exception assert_raise ArgumentError do - Author.belongs_to :special_author_address, :dependent => :restrict + Class.new(Author).belongs_to :special_author_address, :dependent => :restrict end end diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb index 01f7f18397..80bca7f63e 100644 --- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb +++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb @@ -16,7 +16,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase :categorizations, :people, :categories, :edges, :vertices def test_eager_association_loading_with_cascaded_two_levels - authors = Author.scoped(:includes=>{:posts=>:comments}, :order=>"authors.id").all + authors = Author.all.merge!(:includes=>{:posts=>:comments}, :order=>"authors.id").to_a assert_equal 3, authors.size assert_equal 5, authors[0].posts.size assert_equal 3, authors[1].posts.size @@ -24,7 +24,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase end def test_eager_association_loading_with_cascaded_two_levels_and_one_level - authors = Author.scoped(:includes=>[{:posts=>:comments}, :categorizations], :order=>"authors.id").all + authors = Author.all.merge!(:includes=>[{:posts=>:comments}, :categorizations], :order=>"authors.id").to_a assert_equal 3, authors.size assert_equal 5, authors[0].posts.size assert_equal 3, authors[1].posts.size @@ -35,16 +35,16 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase def test_eager_association_loading_with_hmt_does_not_table_name_collide_when_joining_associations assert_nothing_raised do - Author.joins(:posts).eager_load(:comments).where(:posts => {:taggings_count => 1}).all + Author.joins(:posts).eager_load(:comments).where(:posts => {:taggings_count => 1}).to_a end - authors = Author.joins(:posts).eager_load(:comments).where(:posts => {:taggings_count => 1}).all + authors = Author.joins(:posts).eager_load(:comments).where(:posts => {:taggings_count => 1}).to_a assert_equal 1, assert_no_queries { authors.size } assert_equal 10, assert_no_queries { authors[0].comments.size } end def test_eager_association_loading_grafts_stashed_associations_to_correct_parent assert_nothing_raised do - Person.eager_load(:primary_contact => :primary_contact).where('primary_contacts_people_2.first_name = ?', 'Susan').order('people.id').all + Person.eager_load(:primary_contact => :primary_contact).where('primary_contacts_people_2.first_name = ?', 'Susan').order('people.id').to_a end assert_equal people(:michael), Person.eager_load(:primary_contact => :primary_contact).where('primary_contacts_people_2.first_name = ?', 'Susan').order('people.id').first end @@ -54,9 +54,9 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase assert_nothing_raised do assert_equal 4, categories.count - assert_equal 4, categories.all.count + assert_equal 4, categories.to_a.count assert_equal 3, categories.count(:distinct => true) - assert_equal 3, categories.all.uniq.size # Must uniq since instantiating with inner joins will get dupes + assert_equal 3, categories.to_a.uniq.size # Must uniq since instantiating with inner joins will get dupes end end @@ -64,7 +64,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase categories = Category.includes(:categorizations).includes(:categorizations => :author).where("categorizations.id is not null").references(:categorizations) assert_nothing_raised do assert_equal 3, categories.count - assert_equal 3, categories.all.size + assert_equal 3, categories.to_a.size end end @@ -72,7 +72,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase categories = Category.includes(:categorizations => :author).includes(:categorizations => :post).where("posts.id is not null").references(:posts) assert_nothing_raised do assert_equal 3, categories.count - assert_equal 3, categories.all.size + assert_equal 3, categories.to_a.size end end @@ -80,11 +80,11 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase authors = Author.joins(:special_posts).includes([:posts, :categorizations]) assert_nothing_raised { authors.count } - assert_queries(3) { authors.all } + assert_queries(3) { authors.to_a } end def test_eager_association_loading_with_cascaded_two_levels_with_two_has_many_associations - authors = Author.scoped(:includes=>{:posts=>[:comments, :categorizations]}, :order=>"authors.id").all + authors = Author.all.merge!(:includes=>{:posts=>[:comments, :categorizations]}, :order=>"authors.id").to_a assert_equal 3, authors.size assert_equal 5, authors[0].posts.size assert_equal 3, authors[1].posts.size @@ -92,7 +92,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase end def test_eager_association_loading_with_cascaded_two_levels_and_self_table_reference - authors = Author.scoped(:includes=>{:posts=>[:comments, :author]}, :order=>"authors.id").all + authors = Author.all.merge!(:includes=>{:posts=>[:comments, :author]}, :order=>"authors.id").to_a assert_equal 3, authors.size assert_equal 5, authors[0].posts.size assert_equal authors(:david).name, authors[0].name @@ -100,13 +100,13 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase end def test_eager_association_loading_with_cascaded_two_levels_with_condition - authors = Author.scoped(:includes=>{:posts=>:comments}, :where=>"authors.id=1", :order=>"authors.id").all + authors = Author.all.merge!(:includes=>{:posts=>:comments}, :where=>"authors.id=1", :order=>"authors.id").to_a assert_equal 1, authors.size assert_equal 5, authors[0].posts.size end def test_eager_association_loading_with_cascaded_three_levels_by_ping_pong - firms = Firm.scoped(:includes=>{:account=>{:firm=>:account}}, :order=>"companies.id").all + firms = Firm.all.merge!(:includes=>{:account=>{:firm=>:account}}, :order=>"companies.id").to_a assert_equal 2, firms.size assert_equal firms.first.account, firms.first.account.firm.account assert_equal companies(:first_firm).account, assert_no_queries { firms.first.account.firm.account } @@ -114,7 +114,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase end def test_eager_association_loading_with_has_many_sti - topics = Topic.scoped(:includes => :replies, :order => 'topics.id').all + topics = Topic.all.merge!(:includes => :replies, :order => 'topics.id').to_a first, second, = topics(:first).replies.size, topics(:second).replies.size assert_no_queries do assert_equal first, topics[0].replies.size @@ -127,7 +127,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase silly.parent_id = 1 assert silly.save - topics = Topic.scoped(:includes => :replies, :order => ['topics.id', 'replies_topics.id']).all + topics = Topic.all.merge!(:includes => :replies, :order => ['topics.id', 'replies_topics.id']).to_a assert_no_queries do assert_equal 2, topics[0].replies.size assert_equal 0, topics[1].replies.size @@ -135,14 +135,14 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase end def test_eager_association_loading_with_belongs_to_sti - replies = Reply.scoped(:includes => :topic, :order => 'topics.id').all + replies = Reply.all.merge!(:includes => :topic, :order => 'topics.id').to_a assert replies.include?(topics(:second)) assert !replies.include?(topics(:first)) assert_equal topics(:first), assert_no_queries { replies.first.topic } end def test_eager_association_loading_with_multiple_stis_and_order - author = Author.scoped(:includes => { :posts => [ :special_comments , :very_special_comment ] }, :order => ['authors.name', 'comments.body', 'very_special_comments_posts.body'], :where => 'posts.id = 4').first + author = Author.all.merge!(:includes => { :posts => [ :special_comments , :very_special_comment ] }, :order => ['authors.name', 'comments.body', 'very_special_comments_posts.body'], :where => 'posts.id = 4').first assert_equal authors(:david), author assert_no_queries do author.posts.first.special_comments @@ -151,7 +151,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase end def test_eager_association_loading_of_stis_with_multiple_references - authors = Author.scoped(:includes => { :posts => { :special_comments => { :post => [ :special_comments, :very_special_comment ] } } }, :order => 'comments.body, very_special_comments_posts.body', :where => 'posts.id = 4').all + authors = Author.all.merge!(:includes => { :posts => { :special_comments => { :post => [ :special_comments, :very_special_comment ] } } }, :order => 'comments.body, very_special_comments_posts.body', :where => 'posts.id = 4').to_a assert_equal [authors(:david)], authors assert_no_queries do authors.first.posts.first.special_comments.first.post.special_comments @@ -160,7 +160,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase end def test_eager_association_loading_where_first_level_returns_nil - authors = Author.scoped(:includes => {:post_about_thinking => :comments}, :order => 'authors.id DESC').all + authors = Author.all.merge!(:includes => {:post_about_thinking => :comments}, :order => 'authors.id DESC').to_a assert_equal [authors(:bob), authors(:mary), authors(:david)], authors assert_no_queries do authors[2].post_about_thinking.comments.first @@ -168,12 +168,12 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase end def test_eager_association_loading_with_recursive_cascading_four_levels_has_many_through - source = Vertex.scoped(:includes=>{:sinks=>{:sinks=>{:sinks=>:sinks}}}, :order => 'vertices.id').first + source = Vertex.all.merge!(:includes=>{:sinks=>{:sinks=>{:sinks=>:sinks}}}, :order => 'vertices.id').first assert_equal vertices(:vertex_4), assert_no_queries { source.sinks.first.sinks.first.sinks.first } end def test_eager_association_loading_with_recursive_cascading_four_levels_has_and_belongs_to_many - sink = Vertex.scoped(:includes=>{:sources=>{:sources=>{:sources=>:sources}}}, :order => 'vertices.id DESC').first + sink = Vertex.all.merge!(:includes=>{:sources=>{:sources=>{:sources=>:sources}}}, :order => 'vertices.id DESC').first assert_equal vertices(:vertex_1), assert_no_queries { sink.sources.first.sources.first.sources.first.sources.first } end end diff --git a/activerecord/test/cases/associations/eager_load_nested_include_test.rb b/activerecord/test/cases/associations/eager_load_nested_include_test.rb index bb0d6bc70b..5ff117eaa0 100644 --- a/activerecord/test/cases/associations/eager_load_nested_include_test.rb +++ b/activerecord/test/cases/associations/eager_load_nested_include_test.rb @@ -92,7 +92,7 @@ class EagerLoadPolyAssocsTest < ActiveRecord::TestCase end def test_include_query - res = ShapeExpression.scoped(:includes => [ :shape, { :paint => :non_poly } ]).all + res = ShapeExpression.all.merge!(:includes => [ :shape, { :paint => :non_poly } ]).to_a assert_equal NUM_SHAPE_EXPRESSIONS, res.size assert_queries(0) do res.each do |se| @@ -122,7 +122,7 @@ class EagerLoadNestedIncludeWithMissingDataTest < ActiveRecord::TestCase assert_nothing_raised do # @davey_mcdave doesn't have any author_favorites includes = {:posts => :comments, :categorizations => :category, :author_favorites => :favorite_author } - Author.scoped(:includes => includes, :where => {:authors => {:name => @davey_mcdave.name}}, :order => 'categories.name').to_a + Author.all.merge!(:includes => includes, :where => {:authors => {:name => @davey_mcdave.name}}, :order => 'categories.name').to_a end end end diff --git a/activerecord/test/cases/associations/eager_singularization_test.rb b/activerecord/test/cases/associations/eager_singularization_test.rb index 5805e71249..634f6b63ba 100644 --- a/activerecord/test/cases/associations/eager_singularization_test.rb +++ b/activerecord/test/cases/associations/eager_singularization_test.rb @@ -103,43 +103,43 @@ class EagerSingularizationTest < ActiveRecord::TestCase def test_eager_no_extra_singularization_belongs_to return unless @have_tables assert_nothing_raised do - Virus.scoped(:includes => :octopus).all + Virus.all.merge!(:includes => :octopus).to_a end end def test_eager_no_extra_singularization_has_one return unless @have_tables assert_nothing_raised do - Octopus.scoped(:includes => :virus).all + Octopus.all.merge!(:includes => :virus).to_a end end def test_eager_no_extra_singularization_has_many return unless @have_tables assert_nothing_raised do - Bus.scoped(:includes => :passes).all + Bus.all.merge!(:includes => :passes).to_a end end def test_eager_no_extra_singularization_has_and_belongs_to_many return unless @have_tables assert_nothing_raised do - Crisis.scoped(:includes => :messes).all - Mess.scoped(:includes => :crises).all + Crisis.all.merge!(:includes => :messes).to_a + Mess.all.merge!(:includes => :crises).to_a end end def test_eager_no_extra_singularization_has_many_through_belongs_to return unless @have_tables assert_nothing_raised do - Crisis.scoped(:includes => :successes).all + Crisis.all.merge!(:includes => :successes).to_a end end def test_eager_no_extra_singularization_has_many_through_has_many return unless @have_tables assert_nothing_raised do - Crisis.scoped(:includes => :compresses).all + Crisis.all.merge!(:includes => :compresses).to_a end end end diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index 08467900f9..da4eeb3844 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -29,48 +29,43 @@ class EagerAssociationTest < ActiveRecord::TestCase :owners, :pets, :author_favorites, :jobs, :references, :subscribers, :subscriptions, :books, :developers, :projects, :developers_projects, :members, :memberships, :clubs, :sponsors - def setup - # preheat table existence caches - Comment.find_by_id(1) - end - def test_eager_with_has_one_through_join_model_with_conditions_on_the_through - member = Member.scoped(:includes => :favourite_club).find(members(:some_other_guy).id) + member = Member.all.merge!(:includes => :favourite_club).find(members(:some_other_guy).id) assert_nil member.favourite_club end def test_loading_with_one_association - posts = Post.scoped(:includes => :comments).all + posts = Post.all.merge!(:includes => :comments).to_a post = posts.find { |p| p.id == 1 } assert_equal 2, post.comments.size assert post.comments.include?(comments(:greetings)) - post = Post.scoped(:includes => :comments, :where => "posts.title = 'Welcome to the weblog'").first + post = Post.all.merge!(:includes => :comments, :where => "posts.title = 'Welcome to the weblog'").first assert_equal 2, post.comments.size assert post.comments.include?(comments(:greetings)) - posts = Post.scoped(:includes => :last_comment).all + posts = Post.all.merge!(:includes => :last_comment).to_a post = posts.find { |p| p.id == 1 } assert_equal Post.find(1).last_comment, post.last_comment end def test_loading_with_one_association_with_non_preload - posts = Post.scoped(:includes => :last_comment, :order => 'comments.id DESC').all + posts = Post.all.merge!(:includes => :last_comment, :order => 'comments.id DESC').to_a post = posts.find { |p| p.id == 1 } assert_equal Post.find(1).last_comment, post.last_comment end def test_loading_conditions_with_or - posts = authors(:david).posts.references(:comments).scoped( + posts = authors(:david).posts.references(:comments).merge( :includes => :comments, :where => "comments.body like 'Normal%' OR comments.#{QUOTED_TYPE} = 'SpecialComment'" - ).all + ).to_a assert_nil posts.detect { |p| p.author_id != authors(:david).id }, "expected to find only david's posts" end def test_with_ordering - list = Post.scoped(:includes => :comments, :order => "posts.id DESC").all + list = Post.all.merge!(:includes => :comments, :order => "posts.id DESC").to_a [:other_by_mary, :other_by_bob, :misc_by_mary, :misc_by_bob, :eager_other, :sti_habtm, :sti_post_and_comments, :sti_comments, :authorless, :thinking, :welcome ].each_with_index do |post, index| @@ -84,14 +79,14 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_loading_with_multiple_associations - posts = Post.scoped(:includes => [ :comments, :author, :categories ], :order => "posts.id").all + posts = Post.all.merge!(:includes => [ :comments, :author, :categories ], :order => "posts.id").to_a assert_equal 2, posts.first.comments.size assert_equal 2, posts.first.categories.size assert posts.first.comments.include?(comments(:greetings)) end def test_duplicate_middle_objects - comments = Comment.scoped(:where => 'post_id = 1', :includes => [:post => :author]).all + comments = Comment.all.merge!(:where => 'post_id = 1', :includes => [:post => :author]).to_a assert_no_queries do comments.each {|comment| comment.post.author.name} end @@ -99,25 +94,25 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_preloading_has_many_in_multiple_queries_with_more_ids_than_database_can_handle Post.connection.expects(:in_clause_length).at_least_once.returns(5) - posts = Post.scoped(:includes=>:comments).all + posts = Post.all.merge!(:includes=>:comments).to_a assert_equal 11, posts.size end def test_preloading_has_many_in_one_queries_when_database_has_no_limit_on_ids_it_can_handle Post.connection.expects(:in_clause_length).at_least_once.returns(nil) - posts = Post.scoped(:includes=>:comments).all + posts = Post.all.merge!(:includes=>:comments).to_a assert_equal 11, posts.size end def test_preloading_habtm_in_multiple_queries_with_more_ids_than_database_can_handle Post.connection.expects(:in_clause_length).at_least_once.returns(5) - posts = Post.scoped(:includes=>:categories).all + posts = Post.all.merge!(:includes=>:categories).to_a assert_equal 11, posts.size end def test_preloading_habtm_in_one_queries_when_database_has_no_limit_on_ids_it_can_handle Post.connection.expects(:in_clause_length).at_least_once.returns(nil) - posts = Post.scoped(:includes=>:categories).all + posts = Post.all.merge!(:includes=>:categories).to_a assert_equal 11, posts.size end @@ -154,8 +149,8 @@ class EagerAssociationTest < ActiveRecord::TestCase popular_post.readers.create!(:person => people(:michael)) popular_post.readers.create!(:person => people(:david)) - readers = Reader.scoped(:where => ["post_id = ?", popular_post.id], - :includes => {:post => :comments}).all + readers = Reader.all.merge!(:where => ["post_id = ?", popular_post.id], + :includes => {:post => :comments}).to_a readers.each do |reader| assert_equal [comment], reader.post.comments end @@ -167,8 +162,8 @@ class EagerAssociationTest < ActiveRecord::TestCase car_post.categories << categories(:technology) comment = car_post.comments.create!(:body => "hmm") - categories = Category.scoped(:where => { 'posts.id' => car_post.id }, - :includes => {:posts => :comments}).all + categories = Category.all.merge!(:where => { 'posts.id' => car_post.id }, + :includes => {:posts => :comments}).to_a categories.each do |category| assert_equal [comment], category.posts[0].comments end @@ -186,7 +181,7 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_finding_with_includes_on_has_many_association_with_same_include_includes_only_once author_id = authors(:david).id - author = assert_queries(3) { Author.scoped(:includes => {:posts_with_comments => :comments}).find(author_id) } # find the author, then find the posts, then find the comments + author = assert_queries(3) { Author.all.merge!(:includes => {:posts_with_comments => :comments}).find(author_id) } # find the author, then find the posts, then find the comments author.posts_with_comments.each do |post_with_comments| assert_equal post_with_comments.comments.length, post_with_comments.comments.count assert_nil post_with_comments.comments.to_a.uniq! @@ -197,7 +192,7 @@ class EagerAssociationTest < ActiveRecord::TestCase author = authors(:david) post = author.post_about_thinking_with_last_comment last_comment = post.last_comment - author = assert_queries(3) { Author.scoped(:includes => {:post_about_thinking_with_last_comment => :last_comment}).find(author.id)} # find the author, then find the posts, then find the comments + author = assert_queries(3) { Author.all.merge!(:includes => {:post_about_thinking_with_last_comment => :last_comment}).find(author.id)} # find the author, then find the posts, then find the comments assert_no_queries do assert_equal post, author.post_about_thinking_with_last_comment assert_equal last_comment, author.post_about_thinking_with_last_comment.last_comment @@ -208,7 +203,7 @@ class EagerAssociationTest < ActiveRecord::TestCase post = posts(:welcome) author = post.author author_address = author.author_address - post = assert_queries(3) { Post.scoped(:includes => {:author_with_address => :author_address}).find(post.id) } # find the post, then find the author, then find the address + post = assert_queries(3) { Post.all.merge!(:includes => {:author_with_address => :author_address}).find(post.id) } # find the post, then find the author, then find the address assert_no_queries do assert_equal author, post.author_with_address assert_equal author_address, post.author_with_address.author_address @@ -218,7 +213,7 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_finding_with_includes_on_null_belongs_to_association_with_same_include_includes_only_once post = posts(:welcome) post.update_attributes!(:author => nil) - post = assert_queries(1) { Post.scoped(:includes => {:author_with_address => :author_address}).find(post.id) } # find the post, then find the author which is null so no query for the author or address + post = assert_queries(1) { Post.all.merge!(:includes => {:author_with_address => :author_address}).find(post.id) } # find the post, then find the author which is null so no query for the author or address assert_no_queries do assert_equal nil, post.author_with_address end @@ -227,56 +222,56 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_finding_with_includes_on_null_belongs_to_polymorphic_association sponsor = sponsors(:moustache_club_sponsor_for_groucho) sponsor.update_attributes!(:sponsorable => nil) - sponsor = assert_queries(1) { Sponsor.scoped(:includes => :sponsorable).find(sponsor.id) } + sponsor = assert_queries(1) { Sponsor.all.merge!(:includes => :sponsorable).find(sponsor.id) } assert_no_queries do assert_equal nil, sponsor.sponsorable end end def test_loading_from_an_association - posts = authors(:david).posts.scoped(:includes => :comments, :order => "posts.id").all + posts = authors(:david).posts.merge(:includes => :comments, :order => "posts.id").to_a assert_equal 2, posts.first.comments.size end def test_loading_from_an_association_that_has_a_hash_of_conditions assert_nothing_raised do - Author.scoped(:includes => :hello_posts_with_hash_conditions).all + Author.all.merge!(:includes => :hello_posts_with_hash_conditions).to_a end - assert !Author.scoped(:includes => :hello_posts_with_hash_conditions).find(authors(:david).id).hello_posts.empty? + assert !Author.all.merge!(:includes => :hello_posts_with_hash_conditions).find(authors(:david).id).hello_posts.empty? end def test_loading_with_no_associations - assert_nil Post.scoped(:includes => :author).find(posts(:authorless).id).author + assert_nil Post.all.merge!(:includes => :author).find(posts(:authorless).id).author end def test_nested_loading_with_no_associations assert_nothing_raised do - Post.scoped(:includes => {:author => :author_addresss}).find(posts(:authorless).id) + Post.all.merge!(:includes => {:author => :author_addresss}).find(posts(:authorless).id) end end def test_nested_loading_through_has_one_association - aa = AuthorAddress.scoped(:includes => {:author => :posts}).find(author_addresses(:david_address).id) + aa = AuthorAddress.all.merge!(:includes => {:author => :posts}).find(author_addresses(:david_address).id) assert_equal aa.author.posts.count, aa.author.posts.length end def test_nested_loading_through_has_one_association_with_order - aa = AuthorAddress.scoped(:includes => {:author => :posts}, :order => 'author_addresses.id').find(author_addresses(:david_address).id) + aa = AuthorAddress.all.merge!(:includes => {:author => :posts}, :order => 'author_addresses.id').find(author_addresses(:david_address).id) assert_equal aa.author.posts.count, aa.author.posts.length end def test_nested_loading_through_has_one_association_with_order_on_association - aa = AuthorAddress.scoped(:includes => {:author => :posts}, :order => 'authors.id').find(author_addresses(:david_address).id) + aa = AuthorAddress.all.merge!(:includes => {:author => :posts}, :order => 'authors.id').find(author_addresses(:david_address).id) assert_equal aa.author.posts.count, aa.author.posts.length end def test_nested_loading_through_has_one_association_with_order_on_nested_association - aa = AuthorAddress.scoped(:includes => {:author => :posts}, :order => 'posts.id').find(author_addresses(:david_address).id) + aa = AuthorAddress.all.merge!(:includes => {:author => :posts}, :order => 'posts.id').find(author_addresses(:david_address).id) assert_equal aa.author.posts.count, aa.author.posts.length end def test_nested_loading_through_has_one_association_with_conditions - aa = AuthorAddress.references(:author_addresses).scoped( + aa = AuthorAddress.references(:author_addresses).merge( :includes => {:author => :posts}, :where => "author_addresses.id > 0" ).find author_addresses(:david_address).id @@ -284,7 +279,7 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_nested_loading_through_has_one_association_with_conditions_on_association - aa = AuthorAddress.references(:authors).scoped( + aa = AuthorAddress.references(:authors).merge( :includes => {:author => :posts}, :where => "authors.id > 0" ).find author_addresses(:david_address).id @@ -292,7 +287,7 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_nested_loading_through_has_one_association_with_conditions_on_nested_association - aa = AuthorAddress.references(:posts).scoped( + aa = AuthorAddress.references(:posts).merge( :includes => {:author => :posts}, :where => "posts.id > 0" ).find author_addresses(:david_address).id @@ -300,12 +295,12 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_eager_association_loading_with_belongs_to_and_foreign_keys - pets = Pet.scoped(:includes => :owner).all + pets = Pet.all.merge!(:includes => :owner).to_a assert_equal 3, pets.length end def test_eager_association_loading_with_belongs_to - comments = Comment.scoped(:includes => :post).all + comments = Comment.all.merge!(:includes => :post).to_a assert_equal 11, comments.length titles = comments.map { |c| c.post.title } assert titles.include?(posts(:welcome).title) @@ -313,31 +308,31 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_eager_association_loading_with_belongs_to_and_limit - comments = Comment.scoped(:includes => :post, :limit => 5, :order => 'comments.id').all + comments = Comment.all.merge!(:includes => :post, :limit => 5, :order => 'comments.id').to_a assert_equal 5, comments.length assert_equal [1,2,3,5,6], comments.collect { |c| c.id } end def test_eager_association_loading_with_belongs_to_and_limit_and_conditions - comments = Comment.scoped(:includes => :post, :where => 'post_id = 4', :limit => 3, :order => 'comments.id').all + comments = Comment.all.merge!(:includes => :post, :where => 'post_id = 4', :limit => 3, :order => 'comments.id').to_a assert_equal 3, comments.length assert_equal [5,6,7], comments.collect { |c| c.id } end def test_eager_association_loading_with_belongs_to_and_limit_and_offset - comments = Comment.scoped(:includes => :post, :limit => 3, :offset => 2, :order => 'comments.id').all + comments = Comment.all.merge!(:includes => :post, :limit => 3, :offset => 2, :order => 'comments.id').to_a assert_equal 3, comments.length assert_equal [3,5,6], comments.collect { |c| c.id } end def test_eager_association_loading_with_belongs_to_and_limit_and_offset_and_conditions - comments = Comment.scoped(:includes => :post, :where => 'post_id = 4', :limit => 3, :offset => 1, :order => 'comments.id').all + comments = Comment.all.merge!(:includes => :post, :where => 'post_id = 4', :limit => 3, :offset => 1, :order => 'comments.id').to_a assert_equal 3, comments.length assert_equal [6,7,8], comments.collect { |c| c.id } end def test_eager_association_loading_with_belongs_to_and_limit_and_offset_and_conditions_array - comments = Comment.scoped(:includes => :post, :where => ['post_id = ?',4], :limit => 3, :offset => 1, :order => 'comments.id').all + comments = Comment.all.merge!(:includes => :post, :where => ['post_id = ?',4], :limit => 3, :offset => 1, :order => 'comments.id').to_a assert_equal 3, comments.length assert_equal [6,7,8], comments.collect { |c| c.id } end @@ -345,7 +340,7 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_association_loading_with_belongs_to_and_conditions_string_with_unquoted_table_name assert_nothing_raised do ActiveSupport::Deprecation.silence do - Comment.scoped(:includes => :post, :where => ['posts.id = ?',4]).all + Comment.all.merge!(:includes => :post, :where => ['posts.id = ?',4]).to_a end end end @@ -353,7 +348,7 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_association_loading_with_belongs_to_and_conditions_hash comments = [] assert_nothing_raised do - comments = Comment.scoped(:includes => :post, :where => {:posts => {:id => 4}}, :limit => 3, :order => 'comments.id').all + comments = Comment.all.merge!(:includes => :post, :where => {:posts => {:id => 4}}, :limit => 3, :order => 'comments.id').to_a end assert_equal 3, comments.length assert_equal [5,6,7], comments.collect { |c| c.id } @@ -366,14 +361,14 @@ class EagerAssociationTest < ActiveRecord::TestCase quoted_posts_id= Comment.connection.quote_table_name('posts') + '.' + Comment.connection.quote_column_name('id') assert_nothing_raised do ActiveSupport::Deprecation.silence do - Comment.scoped(:includes => :post, :where => ["#{quoted_posts_id} = ?",4]).all + Comment.all.merge!(:includes => :post, :where => ["#{quoted_posts_id} = ?",4]).to_a end end end def test_eager_association_loading_with_belongs_to_and_order_string_with_unquoted_table_name assert_nothing_raised do - Comment.scoped(:includes => :post, :order => 'posts.id').all + Comment.all.merge!(:includes => :post, :order => 'posts.id').to_a end end @@ -381,25 +376,25 @@ class EagerAssociationTest < ActiveRecord::TestCase quoted_posts_id= Comment.connection.quote_table_name('posts') + '.' + Comment.connection.quote_column_name('id') assert_nothing_raised do ActiveSupport::Deprecation.silence do - Comment.scoped(:includes => :post, :order => quoted_posts_id).all + Comment.all.merge!(:includes => :post, :order => quoted_posts_id).to_a end end end def test_eager_association_loading_with_belongs_to_and_limit_and_multiple_associations - posts = Post.scoped(:includes => [:author, :very_special_comment], :limit => 1, :order => 'posts.id').all + posts = Post.all.merge!(:includes => [:author, :very_special_comment], :limit => 1, :order => 'posts.id').to_a assert_equal 1, posts.length assert_equal [1], posts.collect { |p| p.id } end def test_eager_association_loading_with_belongs_to_and_limit_and_offset_and_multiple_associations - posts = Post.scoped(:includes => [:author, :very_special_comment], :limit => 1, :offset => 1, :order => 'posts.id').all + posts = Post.all.merge!(:includes => [:author, :very_special_comment], :limit => 1, :offset => 1, :order => 'posts.id').to_a assert_equal 1, posts.length assert_equal [2], posts.collect { |p| p.id } end def test_eager_association_loading_with_belongs_to_inferred_foreign_key_from_association_name - author_favorite = AuthorFavorite.scoped(:includes => :favorite_author).first + author_favorite = AuthorFavorite.all.merge!(:includes => :favorite_author).first assert_equal authors(:mary), assert_no_queries { author_favorite.favorite_author } end @@ -410,26 +405,26 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_eager_load_has_one_quotes_table_and_column_names - michael = Person.scoped(:includes => :favourite_reference).find(people(:michael)) + michael = Person.all.merge!(:includes => :favourite_reference).find(people(:michael)) references(:michael_unicyclist) assert_no_queries{ assert_equal references(:michael_unicyclist), michael.favourite_reference} end def test_eager_load_has_many_quotes_table_and_column_names - michael = Person.scoped(:includes => :references).find(people(:michael)) + michael = Person.all.merge!(:includes => :references).find(people(:michael)) references(:michael_magician,:michael_unicyclist) assert_no_queries{ assert_equal references(:michael_magician,:michael_unicyclist), michael.references.sort_by(&:id) } end def test_eager_load_has_many_through_quotes_table_and_column_names - michael = Person.scoped(:includes => :jobs).find(people(:michael)) + michael = Person.all.merge!(:includes => :jobs).find(people(:michael)) jobs(:magician, :unicyclist) assert_no_queries{ assert_equal jobs(:unicyclist, :magician), michael.jobs.sort_by(&:id) } end def test_eager_load_has_many_with_string_keys subscriptions = subscriptions(:webster_awdr, :webster_rfr) - subscriber =Subscriber.scoped(:includes => :subscriptions).find(subscribers(:second).id) + subscriber =Subscriber.all.merge!(:includes => :subscriptions).find(subscribers(:second).id) assert_equal subscriptions, subscriber.subscriptions.sort_by(&:id) end @@ -447,25 +442,25 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_load_has_many_through_with_string_keys books = books(:awdr, :rfr) - subscriber = Subscriber.scoped(:includes => :books).find(subscribers(:second).id) + subscriber = Subscriber.all.merge!(:includes => :books).find(subscribers(:second).id) assert_equal books, subscriber.books.sort_by(&:id) end def test_eager_load_belongs_to_with_string_keys subscriber = subscribers(:second) - subscription = Subscription.scoped(:includes => :subscriber).find(subscriptions(:webster_awdr).id) + subscription = Subscription.all.merge!(:includes => :subscriber).find(subscriptions(:webster_awdr).id) assert_equal subscriber, subscription.subscriber end def test_eager_association_loading_with_explicit_join - posts = Post.scoped(:includes => :comments, :joins => "INNER JOIN authors ON posts.author_id = authors.id AND authors.name = 'Mary'", :limit => 1, :order => 'author_id').all + posts = Post.all.merge!(:includes => :comments, :joins => "INNER JOIN authors ON posts.author_id = authors.id AND authors.name = 'Mary'", :limit => 1, :order => 'author_id').to_a assert_equal 1, posts.length end def test_eager_with_has_many_through - posts_with_comments = people(:michael).posts.scoped(:includes => :comments, :order => 'posts.id').all - posts_with_author = people(:michael).posts.scoped(:includes => :author, :order => 'posts.id').all - posts_with_comments_and_author = people(:michael).posts.scoped(:includes => [ :comments, :author ], :order => 'posts.id').all + posts_with_comments = people(:michael).posts.merge(:includes => :comments, :order => 'posts.id').to_a + posts_with_author = people(:michael).posts.merge(:includes => :author, :order => 'posts.id').to_a + posts_with_comments_and_author = people(:michael).posts.merge(:includes => [ :comments, :author ], :order => 'posts.id').to_a assert_equal 2, posts_with_comments.inject(0) { |sum, post| sum += post.comments.size } assert_equal authors(:david), assert_no_queries { posts_with_author.first.author } assert_equal authors(:david), assert_no_queries { posts_with_comments_and_author.first.author } @@ -476,32 +471,32 @@ class EagerAssociationTest < ActiveRecord::TestCase Post.create!(:author => author, :title => "TITLE", :body => "BODY") author.author_favorites.create(:favorite_author_id => 1) author.author_favorites.create(:favorite_author_id => 2) - posts_with_author_favorites = author.posts.scoped(:includes => :author_favorites).all + posts_with_author_favorites = author.posts.merge(:includes => :author_favorites).to_a assert_no_queries { posts_with_author_favorites.first.author_favorites.first.author_id } end def test_eager_with_has_many_through_an_sti_join_model - author = Author.scoped(:includes => :special_post_comments, :order => 'authors.id').first + author = Author.all.merge!(:includes => :special_post_comments, :order => 'authors.id').first assert_equal [comments(:does_it_hurt)], assert_no_queries { author.special_post_comments } end def test_eager_with_has_many_through_an_sti_join_model_with_conditions_on_both - author = Author.scoped(:includes => :special_nonexistant_post_comments, :order => 'authors.id').first + author = Author.all.merge!(:includes => :special_nonexistant_post_comments, :order => 'authors.id').first assert_equal [], author.special_nonexistant_post_comments end def test_eager_with_has_many_through_join_model_with_conditions - assert_equal Author.scoped(:includes => :hello_post_comments, + assert_equal Author.all.merge!(:includes => :hello_post_comments, :order => 'authors.id').first.hello_post_comments.sort_by(&:id), - Author.scoped(:order => 'authors.id').first.hello_post_comments.sort_by(&:id) + Author.all.merge!(:order => 'authors.id').first.hello_post_comments.sort_by(&:id) end def test_eager_with_has_many_through_join_model_with_conditions_on_top_level - assert_equal comments(:more_greetings), Author.scoped(:includes => :comments_with_order_and_conditions).find(authors(:david).id).comments_with_order_and_conditions.first + assert_equal comments(:more_greetings), Author.all.merge!(:includes => :comments_with_order_and_conditions).find(authors(:david).id).comments_with_order_and_conditions.first end def test_eager_with_has_many_through_join_model_with_include - author_comments = Author.scoped(:includes => :comments_with_include).find(authors(:david).id).comments_with_include.to_a + author_comments = Author.all.merge!(:includes => :comments_with_include).find(authors(:david).id).comments_with_include.to_a assert_no_queries do author_comments.first.post.title end @@ -509,7 +504,7 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_with_has_many_through_with_conditions_join_model_with_include post_tags = Post.find(posts(:welcome).id).misc_tags - eager_post_tags = Post.scoped(:includes => :misc_tags).find(1).misc_tags + eager_post_tags = Post.all.merge!(:includes => :misc_tags).find(1).misc_tags assert_equal post_tags, eager_post_tags end @@ -520,16 +515,16 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_eager_with_has_many_and_limit - posts = Post.scoped(:order => 'posts.id asc', :includes => [ :author, :comments ], :limit => 2).all + posts = Post.all.merge!(:order => 'posts.id asc', :includes => [ :author, :comments ], :limit => 2).to_a assert_equal 2, posts.size assert_equal 3, posts.inject(0) { |sum, post| sum += post.comments.size } end def test_eager_with_has_many_and_limit_and_conditions if current_adapter?(:OpenBaseAdapter) - posts = Post.scoped(:includes => [ :author, :comments ], :limit => 2, :where => "FETCHBLOB(posts.body) = 'hello'", :order => "posts.id").all + posts = Post.all.merge!(:includes => [ :author, :comments ], :limit => 2, :where => "FETCHBLOB(posts.body) = 'hello'", :order => "posts.id").to_a else - posts = Post.scoped(:includes => [ :author, :comments ], :limit => 2, :where => "posts.body = 'hello'", :order => "posts.id").all + posts = Post.all.merge!(:includes => [ :author, :comments ], :limit => 2, :where => "posts.body = 'hello'", :order => "posts.id").to_a end assert_equal 2, posts.size assert_equal [4,5], posts.collect { |p| p.id } @@ -537,9 +532,9 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_with_has_many_and_limit_and_conditions_array if current_adapter?(:OpenBaseAdapter) - posts = Post.scoped(:includes => [ :author, :comments ], :limit => 2, :where => [ "FETCHBLOB(posts.body) = ?", 'hello' ], :order => "posts.id").all + posts = Post.all.merge!(:includes => [ :author, :comments ], :limit => 2, :where => [ "FETCHBLOB(posts.body) = ?", 'hello' ], :order => "posts.id").to_a else - posts = Post.scoped(:includes => [ :author, :comments ], :limit => 2, :where => [ "posts.body = ?", 'hello' ], :order => "posts.id").all + posts = Post.all.merge!(:includes => [ :author, :comments ], :limit => 2, :where => [ "posts.body = ?", 'hello' ], :order => "posts.id").to_a end assert_equal 2, posts.size assert_equal [4,5], posts.collect { |p| p.id } @@ -547,7 +542,7 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_with_has_many_and_limit_and_conditions_array_on_the_eagers posts = ActiveSupport::Deprecation.silence do - Post.scoped(:includes => [ :author, :comments ], :limit => 2, :where => [ "authors.name = ?", 'David' ]).all + Post.all.merge!(:includes => [ :author, :comments ], :limit => 2, :where => [ "authors.name = ?", 'David' ]).to_a end assert_equal 2, posts.size @@ -558,34 +553,34 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_eager_with_has_many_and_limit_and_high_offset - posts = Post.scoped(:includes => [ :author, :comments ], :limit => 2, :offset => 10, :where => { 'authors.name' => 'David' }).all + posts = Post.all.merge!(:includes => [ :author, :comments ], :limit => 2, :offset => 10, :where => { 'authors.name' => 'David' }).to_a assert_equal 0, posts.size end def test_eager_with_has_many_and_limit_and_high_offset_and_multiple_array_conditions assert_queries(1) do posts = Post.references(:authors, :comments). - scoped(:includes => [ :author, :comments ], :limit => 2, :offset => 10, - :where => [ "authors.name = ? and comments.body = ?", 'David', 'go crazy' ]).all + merge(:includes => [ :author, :comments ], :limit => 2, :offset => 10, + :where => [ "authors.name = ? and comments.body = ?", 'David', 'go crazy' ]).to_a assert_equal 0, posts.size end end def test_eager_with_has_many_and_limit_and_high_offset_and_multiple_hash_conditions assert_queries(1) do - posts = Post.scoped(:includes => [ :author, :comments ], :limit => 2, :offset => 10, - :where => { 'authors.name' => 'David', 'comments.body' => 'go crazy' }).all + posts = Post.all.merge!(:includes => [ :author, :comments ], :limit => 2, :offset => 10, + :where => { 'authors.name' => 'David', 'comments.body' => 'go crazy' }).to_a assert_equal 0, posts.size end end def test_count_eager_with_has_many_and_limit_and_high_offset - posts = Post.scoped(:includes => [ :author, :comments ], :limit => 2, :offset => 10, :where => { 'authors.name' => 'David' }).count(:all) + posts = Post.all.merge!(:includes => [ :author, :comments ], :limit => 2, :offset => 10, :where => { 'authors.name' => 'David' }).count(:all) assert_equal 0, posts end def test_eager_with_has_many_and_limit_with_no_results - posts = Post.scoped(:includes => [ :author, :comments ], :limit => 2, :where => "posts.title = 'magic forest'").all + posts = Post.all.merge!(:includes => [ :author, :comments ], :limit => 2, :where => "posts.title = 'magic forest'").to_a assert_equal 0, posts.size end @@ -602,7 +597,7 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_eager_with_has_and_belongs_to_many_and_limit - posts = Post.scoped(:includes => :categories, :order => "posts.id", :limit => 3).all + posts = Post.all.merge!(:includes => :categories, :order => "posts.id", :limit => 3).to_a assert_equal 3, posts.size assert_equal 2, posts[0].categories.size assert_equal 1, posts[1].categories.size @@ -668,7 +663,7 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_eager_association_loading_with_habtm - posts = Post.scoped(:includes => :categories, :order => "posts.id").all + posts = Post.all.merge!(:includes => :categories, :order => "posts.id").to_a assert_equal 2, posts[0].categories.size assert_equal 1, posts[1].categories.size assert_equal 0, posts[2].categories.size @@ -677,23 +672,23 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_eager_with_inheritance - SpecialPost.scoped(:includes => [ :comments ]).all + SpecialPost.all.merge!(:includes => [ :comments ]).to_a end def test_eager_has_one_with_association_inheritance - post = Post.scoped(:includes => [ :very_special_comment ]).find(4) + post = Post.all.merge!(:includes => [ :very_special_comment ]).find(4) assert_equal "VerySpecialComment", post.very_special_comment.class.to_s end def test_eager_has_many_with_association_inheritance - post = Post.scoped(:includes => [ :special_comments ]).find(4) + post = Post.all.merge!(:includes => [ :special_comments ]).find(4) post.special_comments.each do |special_comment| assert special_comment.is_a?(SpecialComment) end end def test_eager_habtm_with_association_inheritance - post = Post.scoped(:includes => [ :special_categories ]).find(6) + post = Post.all.merge!(:includes => [ :special_categories ]).find(6) assert_equal 1, post.special_categories.size post.special_categories.each do |special_category| assert_equal "SpecialCategory", special_category.class.to_s @@ -702,7 +697,7 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_with_has_one_dependent_does_not_destroy_dependent assert_not_nil companies(:first_firm).account - f = Firm.scoped(:includes => :account, + f = Firm.all.merge!(:includes => :account, :where => ["companies.name = ?", "37signals"]).first assert_not_nil f.account assert_equal companies(:first_firm, :reload).account, f.account @@ -717,22 +712,22 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_with_invalid_association_reference assert_raise(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") { - Post.scoped(:includes=> :monkeys ).find(6) + Post.all.merge!(:includes=> :monkeys ).find(6) } assert_raise(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") { - Post.scoped(:includes=>[ :monkeys ]).find(6) + Post.all.merge!(:includes=>[ :monkeys ]).find(6) } assert_raise(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") { - Post.scoped(:includes=>[ 'monkeys' ]).find(6) + Post.all.merge!(:includes=>[ 'monkeys' ]).find(6) } assert_raise(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys, :elephants") { - Post.scoped(:includes=>[ :monkeys, :elephants ]).find(6) + Post.all.merge!(:includes=>[ :monkeys, :elephants ]).find(6) } end def test_eager_with_default_scope developer = EagerDeveloperWithDefaultScope.where(:name => 'David').first - projects = Project.order(:id).all + projects = Project.order(:id).to_a assert_no_queries do assert_equal(projects, developer.projects) end @@ -740,7 +735,7 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_with_default_scope_as_class_method developer = EagerDeveloperWithClassMethodDefaultScope.where(:name => 'David').first - projects = Project.order(:id).all + projects = Project.order(:id).to_a assert_no_queries do assert_equal(projects, developer.projects) end @@ -748,7 +743,7 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_with_default_scope_as_lambda developer = EagerDeveloperWithLambdaDefaultScope.where(:name => 'David').first - projects = Project.order(:id).all + projects = Project.order(:id).to_a assert_no_queries do assert_equal(projects, developer.projects) end @@ -756,7 +751,7 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_with_default_scope_as_block developer = EagerDeveloperWithBlockDefaultScope.where(:name => 'David').first - projects = Project.order(:id).all + projects = Project.order(:id).to_a assert_no_queries do assert_equal(projects, developer.projects) end @@ -764,58 +759,58 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_with_default_scope_as_callable developer = EagerDeveloperWithCallableDefaultScope.where(:name => 'David').first - projects = Project.order(:id).all + projects = Project.order(:id).to_a assert_no_queries do assert_equal(projects, developer.projects) end end def find_all_ordered(className, include=nil) - className.scoped(:order=>"#{className.table_name}.#{className.primary_key}", :includes=>include).all + className.all.merge!(:order=>"#{className.table_name}.#{className.primary_key}", :includes=>include).to_a end def test_limited_eager_with_order assert_equal( posts(:thinking, :sti_comments), - Post.scoped( + Post.all.merge!( :includes => [:author, :comments], :where => { 'authors.name' => 'David' }, :order => 'UPPER(posts.title)', :limit => 2, :offset => 1 - ).all + ).to_a ) assert_equal( posts(:sti_post_and_comments, :sti_comments), - Post.scoped( + Post.all.merge!( :includes => [:author, :comments], :where => { 'authors.name' => 'David' }, :order => 'UPPER(posts.title) DESC', :limit => 2, :offset => 1 - ).all + ).to_a ) end def test_limited_eager_with_multiple_order_columns assert_equal( posts(:thinking, :sti_comments), - Post.scoped( + Post.all.merge!( :includes => [:author, :comments], :where => { 'authors.name' => 'David' }, :order => ['UPPER(posts.title)', 'posts.id'], :limit => 2, :offset => 1 - ).all + ).to_a ) assert_equal( posts(:sti_post_and_comments, :sti_comments), - Post.scoped( + Post.all.merge!( :includes => [:author, :comments], :where => { 'authors.name' => 'David' }, :order => ['UPPER(posts.title) DESC', 'posts.id'], :limit => 2, :offset => 1 - ).all + ).to_a ) end def test_limited_eager_with_numeric_in_association assert_equal( people(:david, :susan), - Person.references(:number1_fans_people).scoped( + Person.references(:number1_fans_people).merge( :includes => [:readers, :primary_contact, :number1_fan], :where => "number1_fans_people.first_name like 'M%'", :order => 'people.id', :limit => 2, :offset => 0 - ).all + ).to_a ) end @@ -828,9 +823,9 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_polymorphic_type_condition - post = Post.scoped(:includes => :taggings).find(posts(:thinking).id) + post = Post.all.merge!(:includes => :taggings).find(posts(:thinking).id) assert post.taggings.include?(taggings(:thinking_general)) - post = SpecialPost.scoped(:includes => :taggings).find(posts(:thinking).id) + post = SpecialPost.all.merge!(:includes => :taggings).find(posts(:thinking).id) assert post.taggings.include?(taggings(:thinking_general)) end @@ -881,13 +876,13 @@ class EagerAssociationTest < ActiveRecord::TestCase end end def test_eager_with_valid_association_as_string_not_symbol - assert_nothing_raised { Post.scoped(:includes => 'comments').all } + assert_nothing_raised { Post.all.merge!(:includes => 'comments').to_a } end def test_eager_with_floating_point_numbers assert_queries(2) do # Before changes, the floating point numbers will be interpreted as table names and will cause this to run in one query - Comment.scoped(:where => "123.456 = 123.456", :includes => :post).all + Comment.all.merge!(:where => "123.456 = 123.456", :includes => :post).to_a end end @@ -941,21 +936,21 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_load_with_sti_sharing_association assert_queries(2) do #should not do 1 query per subclass - Comment.includes(:post).all + Comment.includes(:post).to_a end end def test_conditions_on_join_table_with_include_and_limit - assert_equal 3, Developer.scoped(:includes => 'projects', :where => { 'developers_projects.access_level' => 1 }, :limit => 5).all.size + assert_equal 3, Developer.all.merge!(:includes => 'projects', :where => { 'developers_projects.access_level' => 1 }, :limit => 5).to_a.size end def test_order_on_join_table_with_include_and_limit - assert_equal 5, Developer.scoped(:includes => 'projects', :order => 'developers_projects.joined_on DESC', :limit => 5).all.size + assert_equal 5, Developer.all.merge!(:includes => 'projects', :order => 'developers_projects.joined_on DESC', :limit => 5).to_a.size end def test_eager_loading_with_order_on_joined_table_preloads posts = assert_queries(2) do - Post.scoped(:joins => :comments, :includes => :author, :order => 'comments.id DESC').all + Post.all.merge!(:joins => :comments, :includes => :author, :order => 'comments.id DESC').to_a end assert_equal posts(:eager_other), posts[1] assert_equal authors(:mary), assert_no_queries { posts[1].author} @@ -963,64 +958,60 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_loading_with_conditions_on_joined_table_preloads posts = assert_queries(2) do - Post.scoped(:select => 'distinct posts.*', :includes => :author, :joins => [:comments], :where => "comments.body like 'Thank you%'", :order => 'posts.id').all + Post.all.merge!(:select => 'distinct posts.*', :includes => :author, :joins => [:comments], :where => "comments.body like 'Thank you%'", :order => 'posts.id').to_a end assert_equal [posts(:welcome)], posts assert_equal authors(:david), assert_no_queries { posts[0].author} posts = assert_queries(2) do - Post.scoped(:select => 'distinct posts.*', :includes => :author, :joins => [:comments], :where => "comments.body like 'Thank you%'", :order => 'posts.id').all + Post.all.merge!(:select => 'distinct posts.*', :includes => :author, :joins => [:comments], :where => "comments.body like 'Thank you%'", :order => 'posts.id').to_a end assert_equal [posts(:welcome)], posts assert_equal authors(:david), assert_no_queries { posts[0].author} posts = assert_queries(2) do - Post.scoped(:includes => :author, :joins => {:taggings => :tag}, :where => "tags.name = 'General'", :order => 'posts.id').all + Post.all.merge!(:includes => :author, :joins => {:taggings => :tag}, :where => "tags.name = 'General'", :order => 'posts.id').to_a end assert_equal posts(:welcome, :thinking), posts posts = assert_queries(2) do - Post.scoped(:includes => :author, :joins => {:taggings => {:tag => :taggings}}, :where => "taggings_tags.super_tag_id=2", :order => 'posts.id').all + Post.all.merge!(:includes => :author, :joins => {:taggings => {:tag => :taggings}}, :where => "taggings_tags.super_tag_id=2", :order => 'posts.id').to_a end assert_equal posts(:welcome, :thinking), posts - end def test_eager_loading_with_conditions_on_string_joined_table_preloads posts = assert_queries(2) do - Post.scoped(:select => 'distinct posts.*', :includes => :author, :joins => "INNER JOIN comments on comments.post_id = posts.id", :where => "comments.body like 'Thank you%'", :order => 'posts.id').all + Post.all.merge!(:select => 'distinct posts.*', :includes => :author, :joins => "INNER JOIN comments on comments.post_id = posts.id", :where => "comments.body like 'Thank you%'", :order => 'posts.id').to_a end assert_equal [posts(:welcome)], posts assert_equal authors(:david), assert_no_queries { posts[0].author} posts = assert_queries(2) do - Post.scoped(:select => 'distinct posts.*', :includes => :author, :joins => ["INNER JOIN comments on comments.post_id = posts.id"], :where => "comments.body like 'Thank you%'", :order => 'posts.id').all + Post.all.merge!(:select => 'distinct posts.*', :includes => :author, :joins => ["INNER JOIN comments on comments.post_id = posts.id"], :where => "comments.body like 'Thank you%'", :order => 'posts.id').to_a end assert_equal [posts(:welcome)], posts assert_equal authors(:david), assert_no_queries { posts[0].author} - end def test_eager_loading_with_select_on_joined_table_preloads posts = assert_queries(2) do - Post.scoped(:select => 'posts.*, authors.name as author_name', :includes => :comments, :joins => :author, :order => 'posts.id').all + Post.all.merge!(:select => 'posts.*, authors.name as author_name', :includes => :comments, :joins => :author, :order => 'posts.id').to_a end assert_equal 'David', posts[0].author_name assert_equal posts(:welcome).comments, assert_no_queries { posts[0].comments} end def test_eager_loading_with_conditions_on_join_model_preloads - Author.columns - authors = assert_queries(2) do - Author.scoped(:includes => :author_address, :joins => :comments, :where => "posts.title like 'Welcome%'").all + Author.all.merge!(:includes => :author_address, :joins => :comments, :where => "posts.title like 'Welcome%'").to_a end assert_equal authors(:david), authors[0] assert_equal author_addresses(:david_address), authors[0].author_address end def test_preload_belongs_to_uses_exclusive_scope - people = Person.males.scoped(:includes => :primary_contact).all + people = Person.males.merge(:includes => :primary_contact).to_a assert_not_equal people.length, 0 people.each do |person| assert_no_queries {assert_not_nil person.primary_contact} @@ -1029,7 +1020,7 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_preload_has_many_uses_exclusive_scope - people = Person.males.includes(:agents).all + people = Person.males.includes(:agents).to_a people.each do |person| assert_equal Person.find(person.id).agents, person.agents end @@ -1047,9 +1038,9 @@ class EagerAssociationTest < ActiveRecord::TestCase expected = Firm.find(1).clients_using_primary_key.sort_by(&:name) # Oracle adapter truncates alias to 30 characters if current_adapter?(:OracleAdapter) - firm = Firm.scoped(:includes => :clients_using_primary_key, :order => 'clients_using_primary_keys_companies'[0,30]+'.name').find(1) + firm = Firm.all.merge!(:includes => :clients_using_primary_key, :order => 'clients_using_primary_keys_companies'[0,30]+'.name').find(1) else - firm = Firm.scoped(:includes => :clients_using_primary_key, :order => 'clients_using_primary_keys_companies.name').find(1) + firm = Firm.all.merge!(:includes => :clients_using_primary_key, :order => 'clients_using_primary_keys_companies.name').find(1) end assert_no_queries do assert_equal expected, firm.clients_using_primary_key @@ -1058,7 +1049,7 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_preload_has_one_using_primary_key expected = accounts(:signals37) - firm = Firm.scoped(:includes => :account_using_primary_key, :order => 'companies.id').first + firm = Firm.all.merge!(:includes => :account_using_primary_key, :order => 'companies.id').first assert_no_queries do assert_equal expected, firm.account_using_primary_key end @@ -1066,7 +1057,7 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_include_has_one_using_primary_key expected = accounts(:signals37) - firm = Firm.scoped(:includes => :account_using_primary_key, :order => 'accounts.id').all.detect {|f| f.id == 1} + firm = Firm.all.merge!(:includes => :account_using_primary_key, :order => 'accounts.id').to_a.detect {|f| f.id == 1} assert_no_queries do assert_equal expected, firm.account_using_primary_key end @@ -1130,7 +1121,7 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_deep_including_through_habtm - posts = Post.scoped(:includes => {:categories => :categorizations}, :order => "posts.id").all + posts = Post.all.merge!(:includes => {:categories => :categorizations}, :order => "posts.id").to_a assert_no_queries { assert_equal 2, posts[0].categories[0].categorizations.length } assert_no_queries { assert_equal 1, posts[0].categories[1].categorizations.length } assert_no_queries { assert_equal 2, posts[1].categories[0].categorizations.length } diff --git a/activerecord/test/cases/associations/extension_test.rb b/activerecord/test/cases/associations/extension_test.rb index d7c489c2b5..bd5a426ca8 100644 --- a/activerecord/test/cases/associations/extension_test.rb +++ b/activerecord/test/cases/associations/extension_test.rb @@ -64,14 +64,14 @@ class AssociationsExtensionsTest < ActiveRecord::TestCase def test_proxy_association_after_scoped post = posts(:welcome) assert_equal post.association(:comments), post.comments.the_association - assert_equal post.association(:comments), post.comments.scoped.the_association + assert_equal post.association(:comments), post.comments.where('1=1').the_association end private def extension_name(model) - builder = ActiveRecord::Associations::Builder::HasMany.new(model, :association_name, {}) { } + builder = ActiveRecord::Associations::Builder::HasMany.new(model, :association_name, nil, {}) { } builder.send(:wrap_block_extension) - builder.options[:extend].first.name + builder.extension_module.name 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 ed1caa2ef5..f3520d43e0 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 @@ -67,12 +67,15 @@ end class DeveloperWithCounterSQL < ActiveRecord::Base self.table_name = 'developers' - has_and_belongs_to_many :projects, - :class_name => "DeveloperWithCounterSQL", - :join_table => "developers_projects", - :association_foreign_key => "project_id", - :foreign_key => "developer_id", - :counter_sql => proc { "SELECT COUNT(*) AS count_all FROM projects INNER JOIN developers_projects ON projects.id = developers_projects.project_id WHERE developers_projects.developer_id =#{id}" } + + ActiveSupport::Deprecation.silence do + has_and_belongs_to_many :projects, + :class_name => "DeveloperWithCounterSQL", + :join_table => "developers_projects", + :association_foreign_key => "project_id", + :foreign_key => "developer_id", + :counter_sql => proc { "SELECT COUNT(*) AS count_all FROM projects INNER JOIN developers_projects ON projects.id = developers_projects.project_id WHERE developers_projects.developer_id =#{id}" } + end end class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase @@ -356,7 +359,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_deleting_array david = Developer.find(1) david.projects.reload - david.projects.delete(Project.all) + david.projects.delete(Project.all.to_a) assert_equal 0, david.projects.size assert_equal 0, david.projects(true).size end @@ -423,7 +426,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_destroying_many david = Developer.find(1) david.projects.reload - projects = Project.all + projects = Project.all.to_a assert_no_difference "Project.count" do david.projects.destroy(*projects) @@ -555,21 +558,21 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_find_with_merged_options assert_equal 1, projects(:active_record).limited_developers.size - assert_equal 1, projects(:active_record).limited_developers.all.size - assert_equal 3, projects(:active_record).limited_developers.limit(nil).all.size + assert_equal 1, projects(:active_record).limited_developers.to_a.size + assert_equal 3, projects(:active_record).limited_developers.limit(nil).to_a.size end def test_dynamic_find_should_respect_association_order # Developers are ordered 'name DESC, id DESC' high_id_jamis = projects(:active_record).developers.create(:name => 'Jamis') - assert_equal high_id_jamis, projects(:active_record).developers.scoped(:where => "name = 'Jamis'").first + assert_equal high_id_jamis, projects(:active_record).developers.merge(:where => "name = 'Jamis'").first assert_equal high_id_jamis, projects(:active_record).developers.find_by_name('Jamis') end - def test_find_should_append_to_association_order + def test_find_should_prepend_to_association_order ordered_developers = projects(:active_record).developers.order('projects.id') - assert_equal ['developers.name desc, developers.id desc', 'projects.id'], ordered_developers.order_values + assert_equal ['projects.id', 'developers.name desc, developers.id desc'], ordered_developers.order_values end def test_dynamic_find_all_should_respect_readonly_access @@ -590,7 +593,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase end def test_find_in_association_with_options - developers = projects(:active_record).developers.all + developers = projects(:active_record).developers.to_a assert_equal 3, developers.size assert_equal developers(:poor_jamis), projects(:active_record).developers.where("salary < 10000").first @@ -636,7 +639,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase project = SpecialProject.create("name" => "Special Project") assert developer.save developer.projects << project - developer.update_column("name", "Bruza") + developer.update_columns("name" => "Bruza") assert_equal 1, Developer.connection.select_value(<<-end_sql).to_i SELECT count(*) FROM developers_projects WHERE project_id = #{project.id} @@ -668,7 +671,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_join_table_alias assert_equal( 3, - Developer.references(:developers_projects_join).scoped( + Developer.references(:developers_projects_join).merge( :includes => {:projects => :developers}, :where => 'developers_projects_join.joined_on IS NOT NULL' ).to_a.size @@ -684,7 +687,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal( 3, - Developer.references(:developers_projects_join).scoped( + Developer.references(:developers_projects_join).merge( :includes => {:projects => :developers}, :where => 'developers_projects_join.joined_on IS NOT NULL', :group => group.join(",") ).to_a.size @@ -692,8 +695,8 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase end def test_find_grouped - all_posts_from_category1 = Post.scoped(:where => "category_id = 1", :joins => :categories).all - grouped_posts_of_category1 = Post.scoped(:where => "category_id = 1", :group => "author_id", :select => 'count(posts.id) as posts_count', :joins => :categories).all + all_posts_from_category1 = Post.all.merge!(:where => "category_id = 1", :joins => :categories).to_a + grouped_posts_of_category1 = Post.all.merge!(:where => "category_id = 1", :group => "author_id", :select => 'count(posts.id) as posts_count', :joins => :categories).to_a assert_equal 5, all_posts_from_category1.size assert_equal 2, grouped_posts_of_category1.size end @@ -773,9 +776,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_self_referential_habtm_without_foreign_key_set_should_raise_exception assert_raise(ActiveRecord::HasAndBelongsToManyAssociationForeignKeyNeeded) { - Member.class_eval do - has_and_belongs_to_many :friends, :class_name => "Member", :join_table => "member_friends" - end + SelfMember.new.friends } end @@ -817,11 +818,14 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase # clear cache possibly created by other tests david.projects.reset_column_information - assert_queries(1) { david.projects.columns; david.projects.columns } + assert_queries(:any) { david.projects.columns } + assert_no_queries { david.projects.columns } ## and again to verify that reset_column_information clears the cache correctly david.projects.reset_column_information - assert_queries(1) { david.projects.columns; david.projects.columns } + + assert_queries(:any) { david.projects.columns } + assert_no_queries { david.projects.columns } end def test_attributes_are_being_set_when_initialized_from_habm_association_with_where_clause @@ -840,4 +844,16 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase developer = project.developers.build assert project.developers.include?(developer) end + + test ":insert_sql is deprecated" do + klass = Class.new(ActiveRecord::Base) + def klass.name; 'Foo'; end + assert_deprecated { klass.has_and_belongs_to_many :posts, :insert_sql => 'lol' } + end + + test ":delete_sql is deprecated" do + klass = Class.new(ActiveRecord::Base) + def klass.name; 'Foo'; end + assert_deprecated { klass.has_and_belongs_to_many :posts, :delete_sql => 'lol' } + end end diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 0d8f311117..04714f42e9 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -22,7 +22,9 @@ require 'models/engine' class HasManyAssociationsTestForCountWithFinderSql < ActiveRecord::TestCase class Invoice < ActiveRecord::Base - has_many :custom_line_items, :class_name => 'LineItem', :finder_sql => "SELECT line_items.* from line_items" + ActiveSupport::Deprecation.silence do + has_many :custom_line_items, :class_name => 'LineItem', :finder_sql => "SELECT line_items.* from line_items" + end end def test_should_fail assert_raise(ArgumentError) do @@ -33,7 +35,9 @@ end class HasManyAssociationsTestForCountWithCountSql < ActiveRecord::TestCase class Invoice < ActiveRecord::Base - has_many :custom_line_items, :class_name => 'LineItem', :counter_sql => "SELECT COUNT(*) line_items.* from line_items" + ActiveSupport::Deprecation.silence do + has_many :custom_line_items, :class_name => 'LineItem', :counter_sql => "SELECT COUNT(*) line_items.* from line_items" + end end def test_should_fail assert_raise(ArgumentError) do @@ -44,7 +48,9 @@ end class HasManyAssociationsTestForCountDistinctWithFinderSql < ActiveRecord::TestCase class Invoice < ActiveRecord::Base - has_many :custom_line_items, :class_name => 'LineItem', :finder_sql => "SELECT DISTINCT line_items.amount from line_items" + ActiveSupport::Deprecation.silence do + has_many :custom_line_items, :class_name => 'LineItem', :finder_sql => "SELECT DISTINCT line_items.amount from line_items" + end end def test_should_count_distinct_results @@ -178,7 +184,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase # would be convenient), because this would cause that scope to be applied to any callbacks etc. def test_build_and_create_should_not_happen_within_scope car = cars(:honda) - scoped_count = car.foo_bulbs.scoped.where_values.count + scoped_count = car.foo_bulbs.where_values.count bulb = car.foo_bulbs.build assert_not_equal scoped_count, bulb.scope_after_initialize.where_values.count @@ -193,7 +199,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_no_sql_should_be_fired_if_association_already_loaded Car.create(:name => 'honda') bulbs = Car.first.bulbs - bulbs.inspect # to load all instances of bulbs + bulbs.to_a # to load all instances of bulbs assert_no_queries do bulbs.first() @@ -227,19 +233,19 @@ class HasManyAssociationsTest < ActiveRecord::TestCase # sometimes tests on Oracle fail if ORDER BY is not provided therefore add always :order with :first def test_counting_with_counter_sql - assert_equal 2, Firm.scoped(:order => "id").first.clients.count + assert_equal 2, Firm.all.merge!(:order => "id").first.clients.count end def test_counting - assert_equal 2, Firm.scoped(:order => "id").first.plain_clients.count + assert_equal 2, Firm.all.merge!(:order => "id").first.plain_clients.count end def test_counting_with_single_hash - assert_equal 1, Firm.scoped(:order => "id").first.plain_clients.where(:name => "Microsoft").count + assert_equal 1, Firm.all.merge!(:order => "id").first.plain_clients.where(:name => "Microsoft").count end def test_counting_with_column_name_and_hash - assert_equal 2, Firm.scoped(:order => "id").first.plain_clients.count(:name) + assert_equal 2, Firm.all.merge!(:order => "id").first.plain_clients.count(:name) end def test_counting_with_association_limit @@ -249,7 +255,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_finding - assert_equal 2, Firm.scoped(:order => "id").first.clients.length + assert_equal 2, Firm.all.merge!(:order => "id").first.clients.length end def test_finding_array_compatibility @@ -258,23 +264,23 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_find_with_blank_conditions [[], {}, nil, ""].each do |blank| - assert_equal 2, Firm.scoped(:order => "id").first.clients.where(blank).all.size + assert_equal 2, Firm.all.merge!(:order => "id").first.clients.where(blank).to_a.size end end def test_find_many_with_merged_options assert_equal 1, companies(:first_firm).limited_clients.size - assert_equal 1, companies(:first_firm).limited_clients.all.size - assert_equal 2, companies(:first_firm).limited_clients.limit(nil).all.size + assert_equal 1, companies(:first_firm).limited_clients.to_a.size + assert_equal 2, companies(:first_firm).limited_clients.limit(nil).to_a.size end - def test_find_should_append_to_association_order + def test_find_should_prepend_to_association_order ordered_clients = companies(:first_firm).clients_sorted_desc.order('companies.id') - assert_equal ['id DESC', 'companies.id'], ordered_clients.order_values + assert_equal ['companies.id', 'id DESC'], ordered_clients.order_values end def test_dynamic_find_should_respect_association_order - assert_equal companies(:second_client), companies(:first_firm).clients_sorted_desc.scoped(:where => "type = 'Client'").first + assert_equal companies(:second_client), companies(:first_firm).clients_sorted_desc.where("type = 'Client'").first assert_equal companies(:second_client), companies(:first_firm).clients_sorted_desc.find_by_type('Client') end @@ -284,54 +290,54 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_finding_default_orders - assert_equal "Summit", Firm.scoped(:order => "id").first.clients.first.name + assert_equal "Summit", Firm.all.merge!(:order => "id").first.clients.first.name end def test_finding_with_different_class_name_and_order - assert_equal "Microsoft", Firm.scoped(:order => "id").first.clients_sorted_desc.first.name + assert_equal "Microsoft", Firm.all.merge!(:order => "id").first.clients_sorted_desc.first.name end def test_finding_with_foreign_key - assert_equal "Microsoft", Firm.scoped(:order => "id").first.clients_of_firm.first.name + assert_equal "Microsoft", Firm.all.merge!(:order => "id").first.clients_of_firm.first.name end def test_finding_with_condition - assert_equal "Microsoft", Firm.scoped(:order => "id").first.clients_like_ms.first.name + assert_equal "Microsoft", Firm.all.merge!(:order => "id").first.clients_like_ms.first.name end def test_finding_with_condition_hash - assert_equal "Microsoft", Firm.scoped(:order => "id").first.clients_like_ms_with_hash_conditions.first.name + assert_equal "Microsoft", Firm.all.merge!(:order => "id").first.clients_like_ms_with_hash_conditions.first.name end def test_finding_using_primary_key - assert_equal "Summit", Firm.scoped(:order => "id").first.clients_using_primary_key.first.name + assert_equal "Summit", Firm.all.merge!(:order => "id").first.clients_using_primary_key.first.name end def test_finding_using_sql - firm = Firm.scoped(:order => "id").first + firm = Firm.order("id").first first_client = firm.clients_using_sql.first assert_not_nil first_client assert_equal "Microsoft", first_client.name assert_equal 1, firm.clients_using_sql.size - assert_equal 1, Firm.scoped(:order => "id").first.clients_using_sql.size + assert_equal 1, Firm.order("id").first.clients_using_sql.size end def test_finding_using_sql_take_into_account_only_uniq_ids - firm = Firm.scoped(:order => "id").first + firm = Firm.order("id").first client = firm.clients_using_sql.first assert_equal client, firm.clients_using_sql.find(client.id, client.id) assert_equal client, firm.clients_using_sql.find(client.id, client.id.to_s) end def test_counting_using_sql - assert_equal 1, Firm.scoped(:order => "id").first.clients_using_counter_sql.size - assert Firm.scoped(:order => "id").first.clients_using_counter_sql.any? - assert_equal 0, Firm.scoped(:order => "id").first.clients_using_zero_counter_sql.size - assert !Firm.scoped(:order => "id").first.clients_using_zero_counter_sql.any? + assert_equal 1, Firm.order("id").first.clients_using_counter_sql.size + assert Firm.order("id").first.clients_using_counter_sql.any? + assert_equal 0, Firm.order("id").first.clients_using_zero_counter_sql.size + assert !Firm.order("id").first.clients_using_zero_counter_sql.any? end def test_counting_non_existant_items_using_sql - assert_equal 0, Firm.scoped(:order => "id").first.no_clients_using_counter_sql.size + assert_equal 0, Firm.order("id").first.no_clients_using_counter_sql.size end def test_counting_using_finder_sql @@ -346,7 +352,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_find_ids - firm = Firm.scoped(:order => "id").first + firm = Firm.all.merge!(:order => "id").first assert_raise(ActiveRecord::RecordNotFound) { firm.clients.find } @@ -366,7 +372,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_find_string_ids_when_using_finder_sql - firm = Firm.scoped(:order => "id").first + firm = Firm.order("id").first client = firm.clients_using_finder_sql.find("2") assert_kind_of Client, client @@ -382,9 +388,9 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_find_all - firm = Firm.scoped(:order => "id").first - assert_equal 2, firm.clients.scoped(:where => "#{QUOTED_TYPE} = 'Client'").all.length - assert_equal 1, firm.clients.scoped(:where => "name = 'Summit'").all.length + firm = Firm.all.merge!(:order => "id").first + assert_equal 2, firm.clients.where("#{QUOTED_TYPE} = 'Client'").to_a.length + assert_equal 1, firm.clients.where("name = 'Summit'").to_a.length end def test_find_each @@ -428,29 +434,29 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_find_all_sanitized # sometimes tests on Oracle fail if ORDER BY is not provided therefore add always :order with :first - firm = Firm.scoped(:order => "id").first - summit = firm.clients.scoped(:where => "name = 'Summit'").all - assert_equal summit, firm.clients.scoped(:where => ["name = ?", "Summit"]).all - assert_equal summit, firm.clients.scoped(:where => ["name = :name", { :name => "Summit" }]).all + firm = Firm.all.merge!(:order => "id").first + summit = firm.clients.where("name = 'Summit'").to_a + assert_equal summit, firm.clients.where("name = ?", "Summit").to_a + assert_equal summit, firm.clients.where("name = :name", { :name => "Summit" }).to_a end def test_find_first - firm = Firm.scoped(:order => "id").first + firm = Firm.all.merge!(:order => "id").first client2 = Client.find(2) - assert_equal firm.clients.first, firm.clients.scoped(:order => "id").first - assert_equal client2, firm.clients.scoped(:where => "#{QUOTED_TYPE} = 'Client'", :order => "id").first + assert_equal firm.clients.first, firm.clients.order("id").first + assert_equal client2, firm.clients.where("#{QUOTED_TYPE} = 'Client'").order("id").first end def test_find_first_sanitized - firm = Firm.scoped(:order => "id").first + firm = Firm.all.merge!(:order => "id").first client2 = Client.find(2) - assert_equal client2, firm.clients.scoped(:where => ["#{QUOTED_TYPE} = ?", 'Client'], :order => "id").first - assert_equal client2, firm.clients.scoped(:where => ["#{QUOTED_TYPE} = :type", { :type => 'Client' }], :order => "id").first + assert_equal client2, firm.clients.merge!(:where => ["#{QUOTED_TYPE} = ?", 'Client'], :order => "id").first + assert_equal client2, firm.clients.merge!(:where => ["#{QUOTED_TYPE} = :type", { :type => 'Client' }], :order => "id").first end def test_find_all_with_include_and_conditions assert_nothing_raised do - Developer.scoped(:joins => :audit_logs, :where => {'audit_logs.message' => nil, :name => 'Smith'}).all + Developer.all.merge!(:joins => :audit_logs, :where => {'audit_logs.message' => nil, :name => 'Smith'}).to_a end end @@ -460,8 +466,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_find_grouped - all_clients_of_firm1 = Client.scoped(:where => "firm_id = 1").all - grouped_clients_of_firm1 = Client.scoped(:where => "firm_id = 1", :group => "firm_id", :select => 'firm_id, count(id) as clients_count').all + all_clients_of_firm1 = Client.all.merge!(:where => "firm_id = 1").to_a + grouped_clients_of_firm1 = Client.all.merge!(:where => "firm_id = 1", :group => "firm_id", :select => 'firm_id, count(id) as clients_count').to_a assert_equal 2, all_clients_of_firm1.size assert_equal 1, grouped_clients_of_firm1.size end @@ -519,7 +525,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_create_with_bang_on_has_many_raises_when_record_not_saved assert_raise(ActiveRecord::RecordInvalid) do - firm = Firm.scoped(:order => "id").first + firm = Firm.all.merge!(:order => "id").first firm.plain_clients.create! end end @@ -718,7 +724,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_deleting_updates_counter_cache_with_dependent_delete_all post = posts(:welcome) - post.update_column(:taggings_with_delete_all_count, post.taggings_count) + post.update_columns(taggings_with_delete_all_count: post.taggings_count) assert_difference "post.reload.taggings_with_delete_all_count", -1 do post.taggings_with_delete_all.delete(post.taggings_with_delete_all.first) @@ -727,7 +733,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_deleting_updates_counter_cache_with_dependent_destroy post = posts(:welcome) - post.update_column(:taggings_with_destroy_count, post.taggings_count) + post.update_columns(taggings_with_destroy_count: post.taggings_count) assert_difference "post.reload.taggings_with_destroy_count", -1 do post.taggings_with_destroy.delete(post.taggings_with_destroy.first) @@ -897,7 +903,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase firm = Firm.first # break the vanilla firm_id foreign key assert_equal 2, firm.clients.count - firm.clients.first.update_column(:firm_id, nil) + firm.clients.first.update_columns(firm_id: nil) assert_equal 1, firm.clients(true).count assert_equal 1, firm.clients_using_primary_key_with_delete_all.count old_record = firm.clients_using_primary_key_with_delete_all.first @@ -1023,7 +1029,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase firm = companies(:first_firm) assert_equal 2, firm.clients.size firm.destroy - assert Client.scoped(:where => "firm_id=#{firm.id}").all.empty? + assert Client.all.merge!(:where => "firm_id=#{firm.id}").to_a.empty? end def test_dependence_for_associations_with_hash_condition @@ -1033,7 +1039,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_destroy_dependent_when_deleted_from_association # sometimes tests on Oracle fail if ORDER BY is not provided therefore add always :order with :first - firm = Firm.scoped(:order => "id").first + firm = Firm.all.merge!(:order => "id").first assert_equal 2, firm.clients.size client = firm.clients.first @@ -1061,7 +1067,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase firm.destroy rescue "do nothing" - assert_equal 2, Client.scoped(:where => "firm_id=#{firm.id}").all.size + assert_equal 2, Client.all.merge!(:where => "firm_id=#{firm.id}").to_a.size end def test_dependence_on_account @@ -1085,9 +1091,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_restrict - option_before = ActiveRecord::Base.dependent_restrict_raises - ActiveRecord::Base.dependent_restrict_raises = true - firm = RestrictedFirm.create!(:name => 'restrict') firm.companies.create(:name => 'child') @@ -1095,15 +1098,25 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_raise(ActiveRecord::DeleteRestrictionError) { firm.destroy } assert RestrictedFirm.exists?(:name => 'restrict') assert firm.companies.exists?(:name => 'child') - ensure - ActiveRecord::Base.dependent_restrict_raises = option_before end - def test_restrict_when_dependent_restrict_raises_config_set_to_false - option_before = ActiveRecord::Base.dependent_restrict_raises - ActiveRecord::Base.dependent_restrict_raises = false + def test_restrict_is_deprecated + klass = Class.new(ActiveRecord::Base) + assert_deprecated { klass.has_many :posts, dependent: :restrict } + end - firm = RestrictedFirm.create!(:name => 'restrict') + def test_restrict_with_exception + firm = RestrictedWithExceptionFirm.create!(:name => 'restrict') + firm.companies.create(:name => 'child') + + assert !firm.companies.empty? + assert_raise(ActiveRecord::DeleteRestrictionError) { firm.destroy } + assert RestrictedWithExceptionFirm.exists?(:name => 'restrict') + assert firm.companies.exists?(:name => 'child') + end + + def test_restrict_with_error + firm = RestrictedWithErrorFirm.create!(:name => 'restrict') firm.companies.create(:name => 'child') assert !firm.companies.empty? @@ -1113,10 +1126,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert !firm.errors.empty? assert_equal "Cannot delete record because dependent companies exist", firm.errors[:base].first - assert RestrictedFirm.exists?(:name => 'restrict') + assert RestrictedWithErrorFirm.exists?(:name => 'restrict') assert firm.companies.exists?(:name => 'child') - ensure - ActiveRecord::Base.dependent_restrict_raises = option_before end def test_included_in_collection @@ -1128,7 +1139,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_replace_with_less - firm = Firm.scoped(:order => "id").first + firm = Firm.all.merge!(:order => "id").first firm.clients = [companies(:first_client)] assert firm.save, "Could not save firm" firm.reload @@ -1142,7 +1153,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_replace_with_new - firm = Firm.scoped(:order => "id").first + firm = Firm.all.merge!(:order => "id").first firm.clients = [companies(:second_client), Client.new("name" => "New Client")] firm.save firm.reload @@ -1242,7 +1253,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_dynamic_find_should_respect_association_order_for_through - assert_equal Comment.find(10), authors(:david).comments_desc.scoped(:where => "comments.type = 'SpecialComment'").first + assert_equal Comment.find(10), authors(:david).comments_desc.where("comments.type = 'SpecialComment'").first assert_equal Comment.find(10), authors(:david).comments_desc.find_by_type('SpecialComment') end @@ -1358,7 +1369,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end assert_equal author.essays, Essay.where(writer_id: "David") - end def test_has_many_custom_primary_key @@ -1432,13 +1442,12 @@ class HasManyAssociationsTest < ActiveRecord::TestCase firm = Namespaced::Firm.create({ :name => 'Some Company' }) firm.clients.create({ :name => 'Some Client' }) - stats = Namespaced::Firm.scoped( + stats = Namespaced::Firm.all.merge!( :select => "#{Namespaced::Firm.table_name}.id, COUNT(#{Namespaced::Client.table_name}.id) AS num_clients", :joins => :clients, :group => "#{Namespaced::Firm.table_name}.id" ).find firm.id assert_equal 1, stats.num_clients.to_i - ensure ActiveRecord::Base.store_full_sti_class = old end @@ -1462,14 +1471,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_creating_using_primary_key - firm = Firm.scoped(:order => "id").first + firm = Firm.all.merge!(:order => "id").first client = firm.clients_using_primary_key.create!(:name => 'test') assert_equal firm.name, client.firm_name end def test_defining_has_many_association_with_delete_all_dependency_lazily_evaluates_target_class ActiveRecord::Reflection::AssociationReflection.any_instance.expects(:class_name).never - class_eval <<-EOF + class_eval(<<-EOF, __FILE__, __LINE__ + 1) class DeleteAllModel < ActiveRecord::Base has_many :nonentities, :dependent => :delete_all end @@ -1478,7 +1487,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_defining_has_many_association_with_nullify_dependency_lazily_evaluates_target_class ActiveRecord::Reflection::AssociationReflection.any_instance.expects(:class_name).never - class_eval <<-EOF + class_eval(<<-EOF, __FILE__, __LINE__ + 1) class NullifyModel < ActiveRecord::Base has_many :nonentities, :dependent => :nullify end @@ -1598,18 +1607,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal [bulb1, bulb3], result end - def test_building_has_many_association_with_restrict_dependency - option_before = ActiveRecord::Base.dependent_restrict_raises - ActiveRecord::Base.dependent_restrict_raises = true - - klass = Class.new(ActiveRecord::Base) - - assert_deprecated { klass.has_many :companies, :dependent => :restrict } - assert_not_deprecated { klass.has_many :companies } - ensure - ActiveRecord::Base.dependent_restrict_raises = option_before - end - def test_collection_association_with_private_kernel_method firm = companies(:first_firm) assert_equal [accounts(:signals37)], firm.accounts.open @@ -1640,4 +1637,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase post.taggings_with_delete_all.delete_all end end + + test ":finder_sql is deprecated" do + klass = Class.new(ActiveRecord::Base) + assert_deprecated { klass.has_many :foo, :finder_sql => 'lol' } + end + + test ":counter_sql is deprecated" do + klass = Class.new(ActiveRecord::Base) + assert_deprecated { klass.has_many :foo, :counter_sql => 'lol' } + 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 1c06007d86..36e5ba9660 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -327,7 +327,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase def test_update_counter_caches_on_delete_with_dependent_destroy post = posts(:welcome) tag = post.tags.create!(:name => 'doomed') - post.update_column(:tags_with_destroy_count, post.tags.count) + post.update_columns(tags_with_destroy_count: post.tags.count) assert_difference ['post.reload.taggings_count', 'post.reload.tags_with_destroy_count'], -1 do posts(:welcome).tags_with_destroy.delete(tag) @@ -337,7 +337,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase def test_update_counter_caches_on_delete_with_dependent_nullify post = posts(:welcome) tag = post.tags.create!(:name => 'doomed') - post.update_column(:tags_with_nullify_count, post.tags.count) + post.update_columns(tags_with_nullify_count: post.tags.count) assert_no_difference 'post.reload.taggings_count' do assert_difference 'post.reload.tags_with_nullify_count', -1 do @@ -706,7 +706,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase def test_through_association_readonly_should_be_false assert !people(:michael).posts.first.readonly? - assert !people(:michael).posts.all.first.readonly? + assert !people(:michael).posts.to_a.first.readonly? end def test_can_update_through_association @@ -742,7 +742,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end def test_has_many_through_with_default_scope_on_join_model - assert_equal posts(:welcome).comments.order('id').all, authors(:david).comments_on_first_posts + assert_equal posts(:welcome).comments.order('id').to_a, authors(:david).comments_on_first_posts end def test_create_has_many_through_with_default_scope_on_join_model @@ -813,13 +813,6 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert post[:author_count].nil? end - def test_interpolated_conditions - post = posts(:welcome) - assert !post.tags.empty? - assert_equal post.tags, post.interpolated_tags - assert_equal post.tags, post.interpolated_tags_2 - end - def test_primary_key_option_on_source post = posts(:welcome) category = categories(:general) diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index 88ec65706c..8bc633f2b5 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -25,13 +25,13 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert_queries(1) { assert_nil firm.account } assert_queries(0) { assert_nil firm.account } - firms = Firm.scoped(:includes => :account).all + firms = Firm.all.merge!(:includes => :account).to_a assert_queries(0) { firms.each(&:account) } end def test_with_select assert_equal Firm.find(1).account_with_select.attributes.size, 2 - assert_equal Firm.scoped(:includes => :account_with_select).find(1).account_with_select.attributes.size, 2 + assert_equal Firm.all.merge!(:includes => :account_with_select).find(1).account_with_select.attributes.size, 2 end def test_finding_using_primary_key @@ -156,10 +156,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert_nothing_raised { firm.destroy } end - def test_dependence_with_restrict - option_before = ActiveRecord::Base.dependent_restrict_raises - ActiveRecord::Base.dependent_restrict_raises = true - + def test_restrict firm = RestrictedFirm.create!(:name => 'restrict') firm.create_account(:credit_limit => 10) @@ -168,38 +165,26 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert_raise(ActiveRecord::DeleteRestrictionError) { firm.destroy } assert RestrictedFirm.exists?(:name => 'restrict') assert firm.account.present? - ensure - ActiveRecord::Base.dependent_restrict_raises = option_before end - def test_dependence_with_restrict_with_dependent_restrict_raises_config_set_to_false - option_before = ActiveRecord::Base.dependent_restrict_raises - ActiveRecord::Base.dependent_restrict_raises = false + def test_restrict_is_deprecated + klass = Class.new(ActiveRecord::Base) + assert_deprecated { klass.has_one :post, dependent: :restrict } + end - firm = RestrictedFirm.create!(:name => 'restrict') + def test_restrict_with_exception + firm = RestrictedWithExceptionFirm.create!(:name => 'restrict') firm.create_account(:credit_limit => 10) assert_not_nil firm.account - firm.destroy - - assert !firm.errors.empty? - assert_equal "Cannot delete record because a dependent account exists", firm.errors[:base].first - assert RestrictedFirm.exists?(:name => 'restrict') + assert_raise(ActiveRecord::DeleteRestrictionError) { firm.destroy } + assert RestrictedWithExceptionFirm.exists?(:name => 'restrict') assert firm.account.present? - ensure - ActiveRecord::Base.dependent_restrict_raises = option_before end - def test_dependence_with_restrict_with_dependent_restrict_raises_config_set_to_false_and_attribute_name - old_backend = I18n.backend - I18n.backend = I18n::Backend::Simple.new - I18n.backend.store_translations 'en', :activerecord => {:attributes => {:restricted_firm => {:account => "account model"}}} - - option_before = ActiveRecord::Base.dependent_restrict_raises - ActiveRecord::Base.dependent_restrict_raises = false - - firm = RestrictedFirm.create!(:name => 'restrict') + def test_restrict_with_error + firm = RestrictedWithErrorFirm.create!(:name => 'restrict') firm.create_account(:credit_limit => 10) assert_not_nil firm.account @@ -207,12 +192,9 @@ class HasOneAssociationsTest < ActiveRecord::TestCase firm.destroy assert !firm.errors.empty? - assert_equal "Cannot delete record because a dependent account model exists", firm.errors[:base].first - assert RestrictedFirm.exists?(:name => 'restrict') + assert_equal "Cannot delete record because a dependent account exists", firm.errors[:base].first + assert RestrictedWithErrorFirm.exists?(:name => 'restrict') assert firm.account.present? - ensure - ActiveRecord::Base.dependent_restrict_raises = option_before - I18n.backend = old_backend end def test_successful_build_association @@ -226,7 +208,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase def test_build_and_create_should_not_happen_within_scope pirate = pirates(:blackbeard) - scoped_count = pirate.association(:foo_bulb).scoped.where_values.count + scoped_count = pirate.association(:foo_bulb).scope.where_values.count bulb = pirate.build_foo_bulb assert_not_equal scoped_count, bulb.scope_after_initialize.where_values.count @@ -346,14 +328,14 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert_nothing_raised do Firm.find(@firm.id).save! - Firm.scoped(:includes => :account).find(@firm.id).save! + Firm.all.merge!(:includes => :account).find(@firm.id).save! end @firm.account.destroy assert_nothing_raised do Firm.find(@firm.id).save! - Firm.scoped(:includes => :account).find(@firm.id).save! + Firm.all.merge!(:includes => :account).find(@firm.id).save! end end @@ -524,15 +506,16 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert_equal car.id, bulb.attributes_after_initialize['car_id'] end - def test_building_has_one_association_with_dependent_restrict - option_before = ActiveRecord::Base.dependent_restrict_raises - ActiveRecord::Base.dependent_restrict_raises = true + def test_has_one_transaction + company = companies(:first_firm) + account = Account.find(1) - klass = Class.new(ActiveRecord::Base) + company.account # force loading + assert_no_queries { company.account = account } - assert_deprecated { klass.has_one :account, :dependent => :restrict } - assert_not_deprecated { klass.has_one :account } - ensure - ActiveRecord::Base.dependent_restrict_raises = option_before + company.account = nil + assert_no_queries { company.account = nil } + account = Account.find(2) + assert_queries { company.account = account } end end diff --git a/activerecord/test/cases/associations/has_one_through_associations_test.rb b/activerecord/test/cases/associations/has_one_through_associations_test.rb index 94b9639e57..90c557e886 100644 --- a/activerecord/test/cases/associations/has_one_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb @@ -73,7 +73,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase def test_has_one_through_eager_loading members = assert_queries(3) do #base table, through table, clubs table - Member.scoped(:includes => :club, :where => ["name = ?", "Groucho Marx"]).all + Member.all.merge!(:includes => :club, :where => ["name = ?", "Groucho Marx"]).to_a end assert_equal 1, members.size assert_not_nil assert_no_queries {members[0].club} @@ -81,7 +81,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase def test_has_one_through_eager_loading_through_polymorphic members = assert_queries(3) do #base table, through table, clubs table - Member.scoped(:includes => :sponsor_club, :where => ["name = ?", "Groucho Marx"]).all + Member.all.merge!(:includes => :sponsor_club, :where => ["name = ?", "Groucho Marx"]).to_a end assert_equal 1, members.size assert_not_nil assert_no_queries {members[0].sponsor_club} @@ -89,14 +89,14 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase def test_has_one_through_with_conditions_eager_loading # conditions on the through table - assert_equal clubs(:moustache_club), Member.scoped(:includes => :favourite_club).find(@member.id).favourite_club - memberships(:membership_of_favourite_club).update_column(:favourite, false) - assert_equal nil, Member.scoped(:includes => :favourite_club).find(@member.id).reload.favourite_club + assert_equal clubs(:moustache_club), Member.all.merge!(:includes => :favourite_club).find(@member.id).favourite_club + memberships(:membership_of_favourite_club).update_columns(favourite: false) + assert_equal nil, Member.all.merge!(:includes => :favourite_club).find(@member.id).reload.favourite_club # conditions on the source table - assert_equal clubs(:moustache_club), Member.scoped(:includes => :hairy_club).find(@member.id).hairy_club - clubs(:moustache_club).update_column(:name, "Association of Clean-Shaven Persons") - assert_equal nil, Member.scoped(:includes => :hairy_club).find(@member.id).reload.hairy_club + assert_equal clubs(:moustache_club), Member.all.merge!(:includes => :hairy_club).find(@member.id).hairy_club + clubs(:moustache_club).update_columns(name: "Association of Clean-Shaven Persons") + assert_equal nil, Member.all.merge!(:includes => :hairy_club).find(@member.id).reload.hairy_club end def test_has_one_through_polymorphic_with_source_type @@ -104,14 +104,14 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase end def test_eager_has_one_through_polymorphic_with_source_type - clubs = Club.scoped(:includes => :sponsored_member, :where => ["name = ?","Moustache and Eyebrow Fancier Club"]).all + clubs = Club.all.merge!(:includes => :sponsored_member, :where => ["name = ?","Moustache and Eyebrow Fancier Club"]).to_a # Only the eyebrow fanciers club has a sponsored_member assert_not_nil assert_no_queries {clubs[0].sponsored_member} end def test_has_one_through_nonpreload_eagerloading members = assert_queries(1) do - Member.scoped(:includes => :club, :where => ["members.name = ?", "Groucho Marx"], :order => 'clubs.name').all #force fallback + Member.all.merge!(:includes => :club, :where => ["members.name = ?", "Groucho Marx"], :order => 'clubs.name').to_a #force fallback end assert_equal 1, members.size assert_not_nil assert_no_queries {members[0].club} @@ -119,7 +119,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase def test_has_one_through_nonpreload_eager_loading_through_polymorphic members = assert_queries(1) do - Member.scoped(:includes => :sponsor_club, :where => ["members.name = ?", "Groucho Marx"], :order => 'clubs.name').all #force fallback + Member.all.merge!(:includes => :sponsor_club, :where => ["members.name = ?", "Groucho Marx"], :order => 'clubs.name').to_a #force fallback end assert_equal 1, members.size assert_not_nil assert_no_queries {members[0].sponsor_club} @@ -128,7 +128,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase def test_has_one_through_nonpreload_eager_loading_through_polymorphic_with_more_than_one_through_record Sponsor.new(:sponsor_club => clubs(:crazy_club), :sponsorable => members(:groucho)).save! members = assert_queries(1) do - Member.scoped(:includes => :sponsor_club, :where => ["members.name = ?", "Groucho Marx"], :order => 'clubs.name DESC').all #force fallback + Member.all.merge!(:includes => :sponsor_club, :where => ["members.name = ?", "Groucho Marx"], :order => 'clubs.name DESC').to_a #force fallback end assert_equal 1, members.size assert_not_nil assert_no_queries { members[0].sponsor_club } @@ -197,7 +197,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase @member.member_detail = @member_detail @member.organization = @organization @member_details = assert_queries(3) do - MemberDetail.scoped(:includes => :member_type).all + MemberDetail.all.merge!(:includes => :member_type).to_a end @new_detail = @member_details[0] assert @new_detail.send(:association, :member_type).loaded? @@ -210,14 +210,14 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase assert_nothing_raised do Club.find(@club.id).save! - Club.scoped(:includes => :sponsored_member).find(@club.id).save! + Club.all.merge!(:includes => :sponsored_member).find(@club.id).save! end @club.sponsor.destroy assert_nothing_raised do Club.find(@club.id).save! - Club.scoped(:includes => :sponsored_member).find(@club.id).save! + Club.all.merge!(:includes => :sponsored_member).find(@club.id).save! end end diff --git a/activerecord/test/cases/associations/inner_join_association_test.rb b/activerecord/test/cases/associations/inner_join_association_test.rb index 1d61d5c474..4f246f575e 100644 --- a/activerecord/test/cases/associations/inner_join_association_test.rb +++ b/activerecord/test/cases/associations/inner_join_association_test.rb @@ -71,18 +71,18 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase end def test_count_honors_implicit_inner_joins - real_count = Author.scoped.to_a.sum{|a| a.posts.count } + real_count = Author.all.to_a.sum{|a| a.posts.count } assert_equal real_count, Author.joins(:posts).count, "plain inner join count should match the number of referenced posts records" end def test_calculate_honors_implicit_inner_joins - real_count = Author.scoped.to_a.sum{|a| a.posts.count } + real_count = Author.all.to_a.sum{|a| a.posts.count } assert_equal real_count, Author.joins(:posts).calculate(:count, 'authors.id'), "plain inner join count should match the number of referenced posts records" end def test_calculate_honors_implicit_inner_joins_and_distinct_and_conditions - real_count = Author.scoped.to_a.select {|a| a.posts.any? {|p| p.title =~ /^Welcome/} }.length - authors_with_welcoming_post_titles = Author.scoped(:joins => :posts, :where => "posts.title like 'Welcome%'").calculate(:count, 'authors.id', :distinct => true) + real_count = Author.all.to_a.select {|a| a.posts.any? {|p| p.title =~ /^Welcome/} }.length + authors_with_welcoming_post_titles = Author.all.merge!(:joins => :posts, :where => "posts.title like 'Welcome%'").calculate(:count, 'authors.id', :distinct => true) assert_equal real_count, authors_with_welcoming_post_titles, "inner join and conditions should have only returned authors posting titles starting with 'Welcome'" end diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb index f35ffb2994..aad48e7ce9 100644 --- a/activerecord/test/cases/associations/inverse_associations_test.rb +++ b/activerecord/test/cases/associations/inverse_associations_test.rb @@ -96,7 +96,7 @@ class InverseHasOneTests < ActiveRecord::TestCase def test_parent_instance_should_be_shared_with_eager_loaded_child_on_find - m = Man.scoped(:where => {:name => 'Gordon'}, :includes => :face).first + m = Man.all.merge!(:where => {:name => 'Gordon'}, :includes => :face).first f = m.face assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance" m.name = 'Bongo' @@ -104,7 +104,7 @@ class InverseHasOneTests < ActiveRecord::TestCase f.man.name = 'Mungo' assert_equal m.name, f.man.name, "Name of man should be the same after changes to child-owned instance" - m = Man.scoped(:where => {:name => 'Gordon'}, :includes => :face, :order => 'faces.id').first + m = Man.all.merge!(:where => {:name => 'Gordon'}, :includes => :face, :order => 'faces.id').first f = m.face assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance" m.name = 'Bongo' @@ -179,7 +179,7 @@ class InverseHasManyTests < ActiveRecord::TestCase end def test_parent_instance_should_be_shared_with_eager_loaded_children - m = Man.scoped(:where => {:name => 'Gordon'}, :includes => :interests).first + m = Man.all.merge!(:where => {:name => 'Gordon'}, :includes => :interests).first is = m.interests is.each do |i| assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance" @@ -189,7 +189,7 @@ class InverseHasManyTests < ActiveRecord::TestCase assert_equal m.name, i.man.name, "Name of man should be the same after changes to child-owned instance" end - m = Man.scoped(:where => {:name => 'Gordon'}, :includes => :interests, :order => 'interests.id').first + m = Man.all.merge!(:where => {:name => 'Gordon'}, :includes => :interests, :order => 'interests.id').first is = m.interests is.each do |i| assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance" @@ -259,6 +259,12 @@ class InverseHasManyTests < ActiveRecord::TestCase assert_equal m.name, i.man.name, "Name of man should be the same after changes to replaced-child-owned instance" end + def test_parent_instance_should_be_shared_with_first_and_last_child + man = Man.first + assert man.interests.first.man.equal? man + assert man.interests.last.man.equal? man + end + def test_trying_to_use_inverses_that_dont_exist_should_raise_an_error assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Man.first.secret_interests } end @@ -278,7 +284,7 @@ class InverseBelongsToTests < ActiveRecord::TestCase end def test_eager_loaded_child_instance_should_be_shared_with_parent_on_find - f = Face.scoped(:includes => :man, :where => {:description => 'trusting'}).first + f = Face.all.merge!(:includes => :man, :where => {:description => 'trusting'}).first m = f.man assert_equal f.description, m.face.description, "Description of face should be the same before changes to child instance" f.description = 'gormless' @@ -286,7 +292,7 @@ class InverseBelongsToTests < ActiveRecord::TestCase m.face.description = 'pleasing' assert_equal f.description, m.face.description, "Description of face should be the same after changes to parent-owned instance" - f = Face.scoped(:includes => :man, :order => 'men.id', :where => {:description => 'trusting'}).first + f = Face.all.merge!(:includes => :man, :order => 'men.id', :where => {:description => 'trusting'}).first m = f.man assert_equal f.description, m.face.description, "Description of face should be the same before changes to child instance" f.description = 'gormless' @@ -351,7 +357,7 @@ class InversePolymorphicBelongsToTests < ActiveRecord::TestCase fixtures :men, :faces, :interests def test_child_instance_should_be_shared_with_parent_on_find - f = Face.scoped(:where => {:description => 'confused'}).first + f = Face.all.merge!(:where => {:description => 'confused'}).first m = f.polymorphic_man assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same before changes to child instance" f.description = 'gormless' @@ -361,7 +367,7 @@ class InversePolymorphicBelongsToTests < ActiveRecord::TestCase end def test_eager_loaded_child_instance_should_be_shared_with_parent_on_find - f = Face.scoped(:where => {:description => 'confused'}, :includes => :man).first + f = Face.all.merge!(:where => {:description => 'confused'}, :includes => :man).first m = f.polymorphic_man assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same before changes to child instance" f.description = 'gormless' @@ -369,7 +375,7 @@ class InversePolymorphicBelongsToTests < ActiveRecord::TestCase m.polymorphic_face.description = 'pleasing' assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same after changes to parent-owned instance" - f = Face.scoped(:where => {:description => 'confused'}, :includes => :man, :order => 'men.id').first + f = Face.all.merge!(:where => {:description => 'confused'}, :includes => :man, :order => 'men.id').first m = f.polymorphic_man assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same before changes to child instance" f.description = 'gormless' diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb index 783b83631c..86893ec4b3 100644 --- a/activerecord/test/cases/associations/join_model_test.rb +++ b/activerecord/test/cases/associations/join_model_test.rb @@ -1,5 +1,4 @@ require "cases/helper" -require 'active_support/core_ext/object/inclusion' require 'models/tag' require 'models/tagging' require 'models/post' @@ -51,7 +50,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_has_many_uniq_through_find - assert_equal 1, authors(:mary).unique_categorized_posts.all.size + assert_equal 1, authors(:mary).unique_categorized_posts.to_a.size end def test_polymorphic_has_many_going_through_join_model @@ -175,7 +174,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase def test_delete_polymorphic_has_many_with_delete_all assert_equal 1, posts(:welcome).taggings.count - posts(:welcome).taggings.first.update_column :taggable_type, 'PostWithHasManyDeleteAll' + posts(:welcome).taggings.first.update_columns taggable_type: 'PostWithHasManyDeleteAll' post = find_post_with_dependency(1, :has_many, :taggings, :delete_all) old_count = Tagging.count @@ -186,7 +185,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase def test_delete_polymorphic_has_many_with_destroy assert_equal 1, posts(:welcome).taggings.count - posts(:welcome).taggings.first.update_column :taggable_type, 'PostWithHasManyDestroy' + posts(:welcome).taggings.first.update_columns taggable_type: 'PostWithHasManyDestroy' post = find_post_with_dependency(1, :has_many, :taggings, :destroy) old_count = Tagging.count @@ -197,7 +196,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase def test_delete_polymorphic_has_many_with_nullify assert_equal 1, posts(:welcome).taggings.count - posts(:welcome).taggings.first.update_column :taggable_type, 'PostWithHasManyNullify' + posts(:welcome).taggings.first.update_columns taggable_type: 'PostWithHasManyNullify' post = find_post_with_dependency(1, :has_many, :taggings, :nullify) old_count = Tagging.count @@ -208,7 +207,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase def test_delete_polymorphic_has_one_with_destroy assert posts(:welcome).tagging - posts(:welcome).tagging.update_column :taggable_type, 'PostWithHasOneDestroy' + posts(:welcome).tagging.update_columns taggable_type: 'PostWithHasOneDestroy' post = find_post_with_dependency(1, :has_one, :tagging, :destroy) old_count = Tagging.count @@ -219,7 +218,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase def test_delete_polymorphic_has_one_with_nullify assert posts(:welcome).tagging - posts(:welcome).tagging.update_column :taggable_type, 'PostWithHasOneNullify' + posts(:welcome).tagging.update_columns taggable_type: 'PostWithHasOneNullify' post = find_post_with_dependency(1, :has_one, :tagging, :nullify) old_count = Tagging.count @@ -233,8 +232,8 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_include_has_many_through - posts = Post.scoped(:order => 'posts.id').all - posts_with_authors = Post.scoped(:includes => :authors, :order => 'posts.id').all + posts = Post.all.merge!(:order => 'posts.id').to_a + posts_with_authors = Post.all.merge!(:includes => :authors, :order => 'posts.id').to_a assert_equal posts.length, posts_with_authors.length posts.length.times do |i| assert_equal posts[i].authors.length, assert_no_queries { posts_with_authors[i].authors.length } @@ -258,8 +257,8 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_include_polymorphic_has_many_through - posts = Post.scoped(:order => 'posts.id').all - posts_with_tags = Post.scoped(:includes => :tags, :order => 'posts.id').all + posts = Post.all.merge!(:order => 'posts.id').to_a + posts_with_tags = Post.all.merge!(:includes => :tags, :order => 'posts.id').to_a assert_equal posts.length, posts_with_tags.length posts.length.times do |i| assert_equal posts[i].tags.length, assert_no_queries { posts_with_tags[i].tags.length } @@ -267,8 +266,8 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_include_polymorphic_has_many - posts = Post.scoped(:order => 'posts.id').all - posts_with_taggings = Post.scoped(:includes => :taggings, :order => 'posts.id').all + posts = Post.all.merge!(:order => 'posts.id').to_a + posts_with_taggings = Post.all.merge!(:includes => :taggings, :order => 'posts.id').to_a assert_equal posts.length, posts_with_taggings.length posts.length.times do |i| assert_equal posts[i].taggings.length, assert_no_queries { posts_with_taggings[i].taggings.length } @@ -276,7 +275,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_has_many_find_all - assert_equal [categories(:general)], authors(:david).categories.all + assert_equal [categories(:general)], authors(:david).categories.to_a end def test_has_many_find_first @@ -288,8 +287,8 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_has_many_find_conditions - assert_equal categories(:general), authors(:david).categories.scoped(:where => "categories.name = 'General'").first - assert_nil authors(:david).categories.scoped(:where => "categories.name = 'Technology'").first + assert_equal categories(:general), authors(:david).categories.where("categories.name = 'General'").first + assert_nil authors(:david).categories.where("categories.name = 'Technology'").first end def test_has_many_array_methods_called_by_method_missing @@ -355,7 +354,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_eager_has_many_polymorphic_with_source_type - tag_with_include = Tag.scoped(:includes => :tagged_posts).find(tags(:general).id) + tag_with_include = Tag.all.merge!(:includes => :tagged_posts).find(tags(:general).id) desired = posts(:welcome, :thinking) assert_no_queries do # added sort by ID as otherwise test using JRuby was failing as array elements were in different order @@ -365,20 +364,20 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_has_many_through_has_many_find_all - assert_equal comments(:greetings), authors(:david).comments.scoped(:order => 'comments.id').all.first + assert_equal comments(:greetings), authors(:david).comments.order('comments.id').to_a.first end def test_has_many_through_has_many_find_all_with_custom_class - assert_equal comments(:greetings), authors(:david).funky_comments.scoped(:order => 'comments.id').all.first + assert_equal comments(:greetings), authors(:david).funky_comments.order('comments.id').to_a.first end def test_has_many_through_has_many_find_first - assert_equal comments(:greetings), authors(:david).comments.scoped(:order => 'comments.id').first + assert_equal comments(:greetings), authors(:david).comments.order('comments.id').first end def test_has_many_through_has_many_find_conditions options = { :where => "comments.#{QUOTED_TYPE}='SpecialComment'", :order => 'comments.id' } - assert_equal comments(:does_it_hurt), authors(:david).comments.scoped(options).first + assert_equal comments(:does_it_hurt), authors(:david).comments.merge(options).first end def test_has_many_through_has_many_find_by_id @@ -386,7 +385,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_has_many_through_polymorphic_has_one - assert_equal Tagging.find(1,2).sort_by { |t| t.id }, authors(:david).tagging + assert_equal Tagging.find(1,2).sort_by { |t| t.id }, authors(:david).taggings_2 end def test_has_many_through_polymorphic_has_many @@ -402,7 +401,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_eager_load_has_many_through_has_many - author = Author.scoped(:where => ['name = ?', 'David'], :includes => :comments, :order => 'comments.id').first + author = Author.all.merge!(:where => ['name = ?', 'David'], :includes => :comments, :order => 'comments.id').first SpecialComment.new; VerySpecialComment.new assert_no_queries do assert_equal [1,2,3,5,6,7,8,9,10,12], author.comments.collect(&:id) @@ -410,7 +409,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_eager_load_has_many_through_has_many_with_conditions - post = Post.scoped(:includes => :invalid_tags).first + post = Post.all.merge!(:includes => :invalid_tags).first assert_no_queries do post.invalid_tags end @@ -418,8 +417,8 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase def test_eager_belongs_to_and_has_one_not_singularized assert_nothing_raised do - Author.scoped(:includes => :author_address).first - AuthorAddress.scoped(:includes => :author).first + Author.all.merge!(:includes => :author_address).first + AuthorAddress.all.merge!(:includes => :author).first end end @@ -454,7 +453,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase assert saved_post.tags.include?(new_tag) assert new_tag.persisted? - assert new_tag.in?(saved_post.reload.tags(true)) + assert saved_post.reload.tags(true).include?(new_tag) new_post = Post.new(:title => "Association replacmenet works!", :body => "You best believe it.") @@ -467,7 +466,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase new_post.save! assert new_post.persisted? - assert saved_tag.in?(new_post.reload.tags(true)) + assert new_post.reload.tags(true).include?(saved_tag) assert !posts(:thinking).tags.build.persisted? assert !posts(:thinking).tags.new.persisted? @@ -625,7 +624,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase def test_polymorphic_has_many expected = taggings(:welcome_general) - p = Post.scoped(:includes => :taggings).find(posts(:welcome).id) + p = Post.all.merge!(:includes => :taggings).find(posts(:welcome).id) assert_no_queries {assert p.taggings.include?(expected)} assert posts(:welcome).taggings.include?(taggings(:welcome_general)) end @@ -633,18 +632,18 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase def test_polymorphic_has_one expected = posts(:welcome) - tagging = Tagging.scoped(:includes => :taggable).find(taggings(:welcome_general).id) + tagging = Tagging.all.merge!(:includes => :taggable).find(taggings(:welcome_general).id) assert_no_queries { assert_equal expected, tagging.taggable} end def test_polymorphic_belongs_to - p = Post.scoped(:includes => {:taggings => :taggable}).find(posts(:welcome).id) + p = Post.all.merge!(:includes => {:taggings => :taggable}).find(posts(:welcome).id) assert_no_queries {assert_equal posts(:welcome), p.taggings.first.taggable} end def test_preload_polymorphic_has_many_through - posts = Post.scoped(:order => 'posts.id').all - posts_with_tags = Post.scoped(:includes => :tags, :order => 'posts.id').all + posts = Post.all.merge!(:order => 'posts.id').to_a + posts_with_tags = Post.all.merge!(:includes => :tags, :order => 'posts.id').to_a assert_equal posts.length, posts_with_tags.length posts.length.times do |i| assert_equal posts[i].tags.length, assert_no_queries { posts_with_tags[i].tags.length } @@ -652,7 +651,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_preload_polymorph_many_types - taggings = Tagging.scoped(:includes => :taggable, :where => ['taggable_type != ?', 'FakeModel']).all + taggings = Tagging.all.merge!(:includes => :taggable, :where => ['taggable_type != ?', 'FakeModel']).to_a assert_no_queries do taggings.first.taggable.id taggings[1].taggable.id @@ -665,13 +664,13 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase def test_preload_nil_polymorphic_belongs_to assert_nothing_raised do - Tagging.scoped(:includes => :taggable, :where => ['taggable_type IS NULL']).all + Tagging.all.merge!(:includes => :taggable, :where => ['taggable_type IS NULL']).to_a end end def test_preload_polymorphic_has_many - posts = Post.scoped(:order => 'posts.id').all - posts_with_taggings = Post.scoped(:includes => :taggings, :order => 'posts.id').all + posts = Post.all.merge!(:order => 'posts.id').to_a + posts_with_taggings = Post.all.merge!(:includes => :taggings, :order => 'posts.id').to_a assert_equal posts.length, posts_with_taggings.length posts.length.times do |i| assert_equal posts[i].taggings.length, assert_no_queries { posts_with_taggings[i].taggings.length } @@ -679,7 +678,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_belongs_to_shared_parent - comments = Comment.scoped(:includes => :post, :where => 'post_id = 1').all + comments = Comment.all.merge!(:includes => :post, :where => 'post_id = 1').to_a assert_no_queries do assert_equal comments.first.post, comments[1].post end @@ -734,7 +733,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase # create dynamic Post models to allow different dependency options def find_post_with_dependency(post_id, association, association_name, dependency) class_name = "PostWith#{association.to_s.classify}#{dependency.to_s.classify}" - Post.find(post_id).update_column :type, class_name + Post.find(post_id).update_columns type: class_name klass = Object.const_set(class_name, Class.new(ActiveRecord::Base)) klass.table_name = 'posts' klass.send(association, association_name, :as => :taggable, :dependent => dependency) diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb index 1d0550afaf..c0f1945cec 100644 --- a/activerecord/test/cases/associations_test.rb +++ b/activerecord/test/cases/associations_test.rb @@ -67,15 +67,15 @@ class AssociationsTest < ActiveRecord::TestCase ship = Ship.create!(:name => "The good ship Dollypop") part = ship.parts.create!(:name => "Mast") part.mark_for_destruction - ShipPart.find(part.id).update_column(:name, 'Deck') + ShipPart.find(part.id).update_columns(name: 'Deck') ship.parts.send(:load_target) assert_equal 'Deck', ship.parts[0].name end def test_include_with_order_works - assert_nothing_raised {Account.scoped(:order => 'id', :includes => :firm).first} - assert_nothing_raised {Account.scoped(:order => :id, :includes => :firm).first} + assert_nothing_raised {Account.all.merge!(:order => 'id', :includes => :firm).first} + assert_nothing_raised {Account.all.merge!(:order => :id, :includes => :firm).first} end def test_bad_collection_keys @@ -86,7 +86,7 @@ class AssociationsTest < ActiveRecord::TestCase def test_should_construct_new_finder_sql_after_create person = Person.new :first_name => 'clark' - assert_equal [], person.readers.all + assert_equal [], person.readers.to_a person.save! reader = Reader.create! :person => person, :post => Post.new(:title => "foo", :body => "bar") assert person.readers.find(reader.id) @@ -110,7 +110,7 @@ class AssociationsTest < ActiveRecord::TestCase end def test_using_limitable_reflections_helper - using_limitable_reflections = lambda { |reflections| Tagging.scoped.send :using_limitable_reflections?, reflections } + using_limitable_reflections = lambda { |reflections| Tagging.all.send :using_limitable_reflections?, reflections } belongs_to_reflections = [Tagging.reflect_on_association(:tag), Tagging.reflect_on_association(:super_tag)] has_many_reflections = [Tag.reflect_on_association(:taggings), Developer.reflect_on_association(:projects)] mixed_reflections = (belongs_to_reflections + has_many_reflections).uniq @@ -131,7 +131,7 @@ class AssociationsTest < ActiveRecord::TestCase def test_association_with_references firm = companies(:first_firm) - assert_equal ['foo'], firm.association_with_references.scoped.references_values + assert_equal ['foo'], firm.association_with_references.references_values end end @@ -176,7 +176,7 @@ class AssociationProxyTest < ActiveRecord::TestCase david = developers(:david) assert !david.projects.loaded? - david.update_column(:created_at, Time.now) + david.update_columns(created_at: Time.now) assert !david.projects.loaded? end @@ -216,7 +216,14 @@ class AssociationProxyTest < ActiveRecord::TestCase end def test_scoped_allows_conditions - assert developers(:david).projects.scoped(where: 'foo').where_values.include?('foo') + assert developers(:david).projects.merge!(where: 'foo').where_values.include?('foo') + end + + test "getting a scope from an association" do + david = developers(:david) + + assert david.projects.scope.is_a?(ActiveRecord::Relation) + assert_equal david.projects, david.projects.scope end end diff --git a/activerecord/test/cases/attribute_methods/read_test.rb b/activerecord/test/cases/attribute_methods/read_test.rb index 98c38535a6..da5d9d8c2a 100644 --- a/activerecord/test/cases/attribute_methods/read_test.rb +++ b/activerecord/test/cases/attribute_methods/read_test.rb @@ -1,5 +1,4 @@ require "cases/helper" -require 'active_support/core_ext/object/inclusion' require 'thread' module ActiveRecord @@ -15,6 +14,7 @@ module ActiveRecord def self.active_record_super; Base; end def self.base_class; self; end + extend ActiveRecord::Configuration include ActiveRecord::AttributeMethods def self.define_attribute_methods @@ -47,13 +47,13 @@ module ActiveRecord instance = @klass.new @klass.column_names.each do |name| - assert !name.in?(instance.methods.map(&:to_s)) + assert !instance.methods.map(&:to_s).include?(name) end @klass.define_attribute_methods @klass.column_names.each do |name| - assert name.in?(instance.methods.map(&:to_s)), "#{name} is not defined" + assert instance.methods.map(&:to_s).include?(name), "#{name} is not defined" end end diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index 1093fedea1..4bc68acd13 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -1,5 +1,4 @@ require "cases/helper" -require 'active_support/core_ext/object/inclusion' require 'models/minimalistic' require 'models/developer' require 'models/auto_id' @@ -35,7 +34,6 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert t.attribute_present?("written_on") assert !t.attribute_present?("content") assert !t.attribute_present?("author_name") - end def test_attribute_present_with_booleans @@ -484,9 +482,9 @@ class AttributeMethodsTest < ActiveRecord::TestCase Topic.create(:title => 'Budget') # Oracle does not support boolean expressions in SELECT if current_adapter?(:OracleAdapter) - topic = Topic.scoped(:select => "topics.*, 0 as is_test").first + topic = Topic.all.merge!(:select => "topics.*, 0 as is_test").first else - topic = Topic.scoped(:select => "topics.*, 1=2 as is_test").first + topic = Topic.all.merge!(:select => "topics.*, 1=2 as is_test").first end assert !topic.is_test? end @@ -495,9 +493,9 @@ class AttributeMethodsTest < ActiveRecord::TestCase Topic.create(:title => 'Budget') # Oracle does not support boolean expressions in SELECT if current_adapter?(:OracleAdapter) - topic = Topic.scoped(:select => "topics.*, 1 as is_test").first + topic = Topic.all.merge!(:select => "topics.*, 1 as is_test").first else - topic = Topic.scoped(:select => "topics.*, 2=2 as is_test").first + topic = Topic.all.merge!(:select => "topics.*, 2=2 as is_test").first end assert topic.is_test? end @@ -792,6 +790,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase end private + def cached_columns Topic.columns.find_all { |column| !Topic.serialized_attributes.include? column.name @@ -815,7 +814,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase end def privatize(method_signature) - @target.class_eval <<-private_method + @target.class_eval(<<-private_method, __FILE__, __LINE__ + 1) private def #{method_signature} "I'm private" diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index 8ef3bfef15..fd4f09ab36 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -20,22 +20,6 @@ require 'models/company' require 'models/eye' class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase - def test_autosave_should_be_a_valid_option_for_has_one - assert ActiveRecord::Associations::Builder::HasOne.valid_options.include?(:autosave) - end - - def test_autosave_should_be_a_valid_option_for_belongs_to - assert ActiveRecord::Associations::Builder::BelongsTo.valid_options.include?(:autosave) - end - - def test_autosave_should_be_a_valid_option_for_has_many - assert ActiveRecord::Associations::Builder::HasMany.valid_options.include?(:autosave) - end - - def test_autosave_should_be_a_valid_option_for_has_and_belongs_to_many - assert ActiveRecord::Associations::Builder::HasAndBelongsToMany.valid_options.include?(:autosave) - end - def test_should_not_add_the_same_callbacks_multiple_times_for_has_one assert_no_difference_when_adding_callbacks_twice_for Pirate, :ship end @@ -155,7 +139,7 @@ class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCas end def test_not_resaved_when_unchanged - firm = Firm.scoped(:includes => :account).first + firm = Firm.all.merge!(:includes => :account).first firm.name += '-changed' assert_queries(1) { firm.save! } diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index f95230ff50..63981a68a9 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -81,6 +81,12 @@ end class BasicsTest < ActiveRecord::TestCase fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors, :categorizations, :categories, :posts + def setup + ActiveRecord::Base.time_zone_aware_attributes = false + ActiveRecord::Base.default_timezone = :local + Time.zone = nil + end + def test_generated_methods_modules modules = Computer.ancestors assert modules.include?(Computer::GeneratedFeatureMethods) @@ -125,36 +131,36 @@ class BasicsTest < ActiveRecord::TestCase unless current_adapter?(:PostgreSQLAdapter,:OracleAdapter,:SQLServerAdapter) def test_limit_with_comma - assert Topic.limit("1,2").all + assert Topic.limit("1,2").to_a end end def test_limit_without_comma - assert_equal 1, Topic.limit("1").all.length - assert_equal 1, Topic.limit(1).all.length + assert_equal 1, Topic.limit("1").to_a.length + assert_equal 1, Topic.limit(1).to_a.length end def test_invalid_limit assert_raises(ArgumentError) do - Topic.limit("asdfadf").all + Topic.limit("asdfadf").to_a end end def test_limit_should_sanitize_sql_injection_for_limit_without_comas assert_raises(ArgumentError) do - Topic.limit("1 select * from schema").all + Topic.limit("1 select * from schema").to_a end end def test_limit_should_sanitize_sql_injection_for_limit_with_comas assert_raises(ArgumentError) do - Topic.limit("1, 7 procedure help()").all + Topic.limit("1, 7 procedure help()").to_a end end unless current_adapter?(:MysqlAdapter) || current_adapter?(:Mysql2Adapter) def test_limit_should_allow_sql_literal - assert_equal 1, Topic.limit(Arel.sql('2-1')).all.length + assert_equal 1, Topic.limit(Arel.sql('2-1')).to_a.length end end @@ -225,6 +231,7 @@ class BasicsTest < ActiveRecord::TestCase assert_equal 11, Topic.find(1).written_on.sec assert_equal 223300, Topic.find(1).written_on.usec assert_equal 9900, Topic.find(2).written_on.usec + assert_equal 129346, Topic.find(3).written_on.usec end end @@ -343,13 +350,13 @@ class BasicsTest < ActiveRecord::TestCase end def test_load - topics = Topic.scoped(:order => 'id').all + topics = Topic.all.merge!(:order => 'id').to_a assert_equal(4, topics.size) assert_equal(topics(:first).title, topics.first.title) end def test_load_with_condition - topics = Topic.scoped(:where => "author_name = 'Mary'").all + topics = Topic.all.merge!(:where => "author_name = 'Mary'").to_a assert_equal(1, topics.size) assert_equal(topics(:second).title, topics.first.title) @@ -504,7 +511,7 @@ class BasicsTest < ActiveRecord::TestCase end # Oracle, and Sybase do not have a TIME datatype. - unless current_adapter?(:OracleAdapter, :SybaseAdapter, :SQLite3Adapter) + unless current_adapter?(:OracleAdapter, :SybaseAdapter) def test_utc_as_time_zone Topic.default_timezone = :utc attributes = { "bonus_time" => "5:42:00AM" } @@ -597,13 +604,19 @@ class BasicsTest < ActiveRecord::TestCase assert_equal "changed", post.body end + def test_attr_readonly_is_class_level_setting + post = ReadonlyTitlePost.new + assert_raise(NoMethodError) { post._attr_readonly = [:title] } + assert_deprecated { post._attr_readonly } + end + def test_non_valid_identifier_column_name weird = Weird.create('a$b' => 'value') weird.reload assert_equal 'value', weird.send('a$b') assert_equal 'value', weird.read_attribute('a$b') - weird.update_column('a$b', 'value2') + weird.update_columns('a$b' => 'value2') weird.reload assert_equal 'value2', weird.send('a$b') assert_equal 'value2', weird.read_attribute('a$b') @@ -686,7 +699,7 @@ class BasicsTest < ActiveRecord::TestCase } topic = Topic.find(1) topic.attributes = attributes - assert_equal Time.utc(2004, 6, 24, 16, 24, 0), topic.written_on + assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on end def test_multiparameter_attributes_on_time_with_no_date @@ -746,9 +759,6 @@ class BasicsTest < ActiveRecord::TestCase end def test_multiparameter_attributes_on_time_will_ignore_hour_if_missing - ActiveRecord::Base.time_zone_aware_attributes = false - ActiveRecord::Base.default_timezone = :local - Time.zone = nil attributes = { "written_on(1i)" => "2004", "written_on(2i)" => "12", "written_on(3i)" => "12", "written_on(5i)" => "12", "written_on(6i)" => "02" @@ -796,8 +806,6 @@ class BasicsTest < ActiveRecord::TestCase topic = Topic.find(1) topic.attributes = attributes assert_equal Time.utc(2004, 6, 24, 16, 24, 0), topic.written_on - ensure - ActiveRecord::Base.default_timezone = :local end def test_multiparameter_attributes_on_time_with_time_zone_aware_attributes @@ -813,14 +821,9 @@ class BasicsTest < ActiveRecord::TestCase assert_equal Time.utc(2004, 6, 24, 23, 24, 0), topic.written_on assert_equal Time.utc(2004, 6, 24, 16, 24, 0), topic.written_on.time assert_equal Time.zone, topic.written_on.time_zone - ensure - ActiveRecord::Base.time_zone_aware_attributes = false - ActiveRecord::Base.default_timezone = :local - Time.zone = nil end def test_multiparameter_attributes_on_time_with_time_zone_aware_attributes_false - ActiveRecord::Base.time_zone_aware_attributes = false Time.zone = ActiveSupport::TimeZone[-28800] attributes = { "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24", @@ -830,8 +833,6 @@ class BasicsTest < ActiveRecord::TestCase topic.attributes = attributes assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on assert_equal false, topic.written_on.respond_to?(:time_zone) - ensure - Time.zone = nil end def test_multiparameter_attributes_on_time_with_skip_time_zone_conversion_for_attributes @@ -848,14 +849,11 @@ class BasicsTest < ActiveRecord::TestCase assert_equal Time.utc(2004, 6, 24, 16, 24, 0), topic.written_on assert_equal false, topic.written_on.respond_to?(:time_zone) ensure - ActiveRecord::Base.time_zone_aware_attributes = false - ActiveRecord::Base.default_timezone = :local - Time.zone = nil Topic.skip_time_zone_conversion_for_attributes = [] end # Oracle, and Sybase do not have a TIME datatype. - unless current_adapter?(:OracleAdapter, :SybaseAdapter, :SQLite3Adapter) + unless current_adapter?(:OracleAdapter, :SybaseAdapter) def test_multiparameter_attributes_on_time_only_column_with_time_zone_aware_attributes_does_not_do_time_zone_conversion ActiveRecord::Base.time_zone_aware_attributes = true ActiveRecord::Base.default_timezone = :utc @@ -868,17 +866,10 @@ class BasicsTest < ActiveRecord::TestCase topic.attributes = attributes assert_equal Time.utc(2000, 1, 1, 16, 24, 0), topic.bonus_time assert topic.bonus_time.utc? - ensure - ActiveRecord::Base.time_zone_aware_attributes = false - ActiveRecord::Base.default_timezone = :local - Time.zone = nil end end def test_multiparameter_attributes_on_time_with_empty_seconds - ActiveRecord::Base.time_zone_aware_attributes = false - ActiveRecord::Base.default_timezone = :local - Time.zone = nil attributes = { "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24", "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "" @@ -888,6 +879,41 @@ class BasicsTest < ActiveRecord::TestCase assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on end + def test_multiparameter_attributes_setting_time_attribute + return skip "Oracle does not have TIME data type" if current_adapter? :OracleAdapter + + topic = Topic.new( "bonus_time(4i)"=> "01", "bonus_time(5i)" => "05" ) + assert_equal 1, topic.bonus_time.hour + assert_equal 5, topic.bonus_time.min + end + + def test_multiparameter_attributes_setting_date_attribute + topic = Topic.new( "written_on(1i)" => "1952", "written_on(2i)" => "3", "written_on(3i)" => "11" ) + assert_equal 1952, topic.written_on.year + assert_equal 3, topic.written_on.month + assert_equal 11, topic.written_on.day + end + + def test_multiparameter_attributes_setting_date_and_time_attribute + topic = Topic.new( + "written_on(1i)" => "1952", + "written_on(2i)" => "3", + "written_on(3i)" => "11", + "written_on(4i)" => "13", + "written_on(5i)" => "55") + assert_equal 1952, topic.written_on.year + assert_equal 3, topic.written_on.month + assert_equal 11, topic.written_on.day + assert_equal 13, topic.written_on.hour + assert_equal 55, topic.written_on.min + end + + def test_multiparameter_attributes_setting_time_but_not_date_on_date_field + assert_raise( ActiveRecord::MultiparameterAssignmentErrors ) do + Topic.new( "written_on(4i)" => "13", "written_on(5i)" => "55" ) + end + end + def test_multiparameter_assignment_of_aggregation customer = Customer.new address = Address.new("The Street", "The City", "The Country") @@ -929,19 +955,20 @@ class BasicsTest < ActiveRecord::TestCase attributes = { "address(1)" => "The Street", "address(2)" => address.city, "address(3000)" => address.country } customer.attributes = attributes end + assert_equal("address", ex.errors[0].attribute) end def test_attributes_on_dummy_time # Oracle, and Sybase do not have a TIME datatype. - return true if current_adapter?(:OracleAdapter, :SybaseAdapter, :SQLite3Adapter) + return true if current_adapter?(:OracleAdapter, :SybaseAdapter) attributes = { "bonus_time" => "5:42:00AM" } topic = Topic.find(1) topic.attributes = attributes - assert_equal Time.utc(2000, 1, 1, 5, 42, 0), topic.bonus_time + assert_equal Time.local(2000, 1, 1, 5, 42, 0), topic.bonus_time end def test_boolean @@ -1263,10 +1290,10 @@ class BasicsTest < ActiveRecord::TestCase end def test_quoting_arrays - replies = Reply.scoped(:where => [ "id IN (?)", topics(:first).replies.collect(&:id) ]).all + replies = Reply.all.merge!(:where => [ "id IN (?)", topics(:first).replies.collect(&:id) ]).to_a assert_equal topics(:first).replies.size, replies.size - replies = Reply.scoped(:where => [ "id IN (?)", [] ]).all + replies = Reply.all.merge!(:where => [ "id IN (?)", [] ]).to_a assert_equal 0, replies.size end @@ -1523,6 +1550,8 @@ class BasicsTest < ActiveRecord::TestCase after_seq = Joke.sequence_name assert_equal before_seq, after_seq unless before_seq.nil? && after_seq.nil? + ensure + Joke.reset_sequence_name end def test_dont_clear_inheritnce_column_when_setting_explicitly @@ -1599,57 +1628,57 @@ class BasicsTest < ActiveRecord::TestCase def test_no_limit_offset assert_nothing_raised do - Developer.scoped(:offset => 2).all + Developer.all.merge!(:offset => 2).to_a end end def test_find_last last = Developer.last - assert_equal last, Developer.scoped(:order => 'id desc').first + assert_equal last, Developer.all.merge!(:order => 'id desc').first end def test_last - assert_equal Developer.scoped(:order => 'id desc').first, Developer.last + assert_equal Developer.all.merge!(:order => 'id desc').first, Developer.last end def test_all developers = Developer.all - assert_kind_of Array, developers + assert_kind_of ActiveRecord::Relation, developers assert_equal Developer.all, developers end def test_all_with_conditions - assert_equal Developer.scoped(:order => 'id desc').all, Developer.order('id desc').all + assert_equal Developer.all.merge!(:order => 'id desc').to_a, Developer.order('id desc').to_a end def test_find_ordered_last - last = Developer.scoped(:order => 'developers.salary ASC').last - assert_equal last, Developer.scoped(:order => 'developers.salary ASC').all.last + last = Developer.all.merge!(:order => 'developers.salary ASC').last + assert_equal last, Developer.all.merge!(:order => 'developers.salary ASC').to_a.last end def test_find_reverse_ordered_last - last = Developer.scoped(:order => 'developers.salary DESC').last - assert_equal last, Developer.scoped(:order => 'developers.salary DESC').all.last + last = Developer.all.merge!(:order => 'developers.salary DESC').last + assert_equal last, Developer.all.merge!(:order => 'developers.salary DESC').to_a.last end def test_find_multiple_ordered_last - last = Developer.scoped(:order => 'developers.name, developers.salary DESC').last - assert_equal last, Developer.scoped(:order => 'developers.name, developers.salary DESC').all.last + last = Developer.all.merge!(:order => 'developers.name, developers.salary DESC').last + assert_equal last, Developer.all.merge!(:order => 'developers.name, developers.salary DESC').to_a.last end def test_find_keeps_multiple_order_values - combined = Developer.scoped(:order => 'developers.name, developers.salary').all - assert_equal combined, Developer.scoped(:order => ['developers.name', 'developers.salary']).all + combined = Developer.all.merge!(:order => 'developers.name, developers.salary').to_a + assert_equal combined, Developer.all.merge!(:order => ['developers.name', 'developers.salary']).to_a end def test_find_keeps_multiple_group_values - combined = Developer.scoped(:group => 'developers.name, developers.salary, developers.id, developers.created_at, developers.updated_at').all - assert_equal combined, Developer.scoped(:group => ['developers.name', 'developers.salary', 'developers.id', 'developers.created_at', 'developers.updated_at']).all + combined = Developer.all.merge!(:group => 'developers.name, developers.salary, developers.id, developers.created_at, developers.updated_at').to_a + assert_equal combined, Developer.all.merge!(:group => ['developers.name', 'developers.salary', 'developers.id', 'developers.created_at', 'developers.updated_at']).to_a end def test_find_symbol_ordered_last - last = Developer.scoped(:order => :salary).last - assert_equal last, Developer.scoped(:order => :salary).all.last + last = Developer.all.merge!(:order => :salary).last + assert_equal last, Developer.all.merge!(:order => :salary).to_a.last end def test_abstract_class @@ -1662,18 +1691,6 @@ class BasicsTest < ActiveRecord::TestCase assert_nil AbstractCompany.table_name end - def test_base_class - assert_equal LoosePerson, LoosePerson.base_class - assert_equal LooseDescendant, LooseDescendant.base_class - assert_equal TightPerson, TightPerson.base_class - assert_equal TightPerson, TightDescendant.base_class - - assert_equal Post, Post.base_class - assert_equal Post, SpecialPost.base_class - assert_equal Post, StiPost.base_class - assert_equal SubStiPost, SubStiPost.base_class - end - def test_descends_from_active_record assert !ActiveRecord::Base.descends_from_active_record? @@ -1727,6 +1744,12 @@ class BasicsTest < ActiveRecord::TestCase assert_kind_of String, Client.first.to_param end + def test_to_param_returns_id_even_if_not_persisted + client = Client.new + client.id = 1 + assert_equal "1", client.to_param + end + def test_inspect_class assert_equal 'ActiveRecord::Base', ActiveRecord::Base.inspect assert_equal 'LoosePerson(abstract)', LoosePerson.inspect @@ -1743,8 +1766,8 @@ class BasicsTest < ActiveRecord::TestCase end def test_inspect_limited_select_instance - assert_equal %(#<Topic id: 1>), Topic.scoped(:select => 'id', :where => 'id = 1').first.inspect - assert_equal %(#<Topic id: 1, title: "The First Topic">), Topic.scoped(:select => 'id, title', :where => 'id = 1').first.inspect + assert_equal %(#<Topic id: 1>), Topic.all.merge!(:select => 'id', :where => 'id = 1').first.inspect + assert_equal %(#<Topic id: 1, title: "The First Topic">), Topic.all.merge!(:select => 'id, title', :where => 'id = 1').first.inspect end def test_inspect_class_without_table @@ -1857,7 +1880,7 @@ class BasicsTest < ActiveRecord::TestCase def test_current_scope_is_reset Object.const_set :UnloadablePost, Class.new(ActiveRecord::Base) - UnloadablePost.send(:current_scope=, UnloadablePost.scoped) + UnloadablePost.send(:current_scope=, UnloadablePost.all) UnloadablePost.unloadable assert_not_nil Thread.current[:UnloadablePost_current_scope] @@ -1924,8 +1947,6 @@ class BasicsTest < ActiveRecord::TestCase est_key = Developer.first.cache_key assert_equal utc_key, est_key - ensure - ActiveRecord::Base.time_zone_aware_attributes = false end def test_cache_key_format_for_existing_record_with_updated_at @@ -1935,13 +1956,13 @@ class BasicsTest < ActiveRecord::TestCase def test_cache_key_format_for_existing_record_with_nil_updated_at dev = Developer.first - dev.update_attribute(:updated_at, nil) + dev.update_columns(updated_at: nil) assert_match(/\/#{dev.id}$/, dev.cache_key) end def test_uniq_delegates_to_scoped scope = stub - Bird.stubs(:scoped).returns(mock(:uniq => scope)) + Bird.stubs(:all).returns(mock(:uniq => scope)) assert_equal scope, Bird.uniq end @@ -2008,7 +2029,7 @@ class BasicsTest < ActiveRecord::TestCase scope.expects(meth).with(:foo, :bar).returns(record) klass = Class.new(ActiveRecord::Base) - klass.stubs(:scoped => scope) + klass.stubs(:all => scope) assert_equal record, klass.public_send(meth, :foo, :bar) end @@ -2016,6 +2037,6 @@ class BasicsTest < ActiveRecord::TestCase test "scoped can take a values hash" do klass = Class.new(ActiveRecord::Base) - assert_equal ['foo'], klass.scoped(select: 'foo').select_values + assert_equal ['foo'], klass.all.merge!(select: 'foo').select_values end end diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index a279b0e77c..40e712072f 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -40,8 +40,8 @@ class CalculationsTest < ActiveRecord::TestCase end def test_type_cast_calculated_value_should_convert_db_averages_of_fixnum_class_to_decimal - assert_equal 0, NumericData.scoped.send(:type_cast_calculated_value, 0, nil, 'avg') - assert_equal 53.0, NumericData.scoped.send(:type_cast_calculated_value, 53, nil, 'avg') + assert_equal 0, NumericData.all.send(:type_cast_calculated_value, 0, nil, 'avg') + assert_equal 53.0, NumericData.all.send(:type_cast_calculated_value, 53, nil, 'avg') end def test_should_get_maximum_of_field @@ -58,7 +58,16 @@ class CalculationsTest < ActiveRecord::TestCase def test_should_group_by_field c = Account.group(:firm_id).sum(:credit_limit) - [1,6,2].each { |firm_id| assert c.keys.include?(firm_id) } + [1,6,2].each do |firm_id| + assert c.keys.include?(firm_id), "Group #{c.inspect} does not contain firm_id #{firm_id}" + end + end + + def test_should_group_by_arel_attribute + c = Account.group(Account.arel_table[:firm_id]).sum(:credit_limit) + [1,6,2].each do |firm_id| + assert c.keys.include?(firm_id), "Group #{c.inspect} does not contain firm_id #{firm_id}" + end end def test_should_group_by_multiple_fields @@ -82,24 +91,24 @@ class CalculationsTest < ActiveRecord::TestCase end def test_should_order_by_grouped_field - c = Account.scoped(:group => :firm_id, :order => "firm_id").sum(:credit_limit) + c = Account.all.merge!(:group => :firm_id, :order => "firm_id").sum(:credit_limit) assert_equal [1, 2, 6, 9], c.keys.compact end def test_should_order_by_calculation - c = Account.scoped(:group => :firm_id, :order => "sum_credit_limit desc, firm_id").sum(:credit_limit) + c = Account.all.merge!(:group => :firm_id, :order => "sum_credit_limit desc, firm_id").sum(:credit_limit) assert_equal [105, 60, 53, 50, 50], c.keys.collect { |k| c[k] } assert_equal [6, 2, 9, 1], c.keys.compact end def test_should_limit_calculation - c = Account.scoped(:where => "firm_id IS NOT NULL", + c = Account.all.merge!(:where => "firm_id IS NOT NULL", :group => :firm_id, :order => "firm_id", :limit => 2).sum(:credit_limit) assert_equal [1, 2], c.keys.compact end def test_should_limit_calculation_with_offset - c = Account.scoped(:where => "firm_id IS NOT NULL", :group => :firm_id, + c = Account.all.merge!(:where => "firm_id IS NOT NULL", :group => :firm_id, :order => "firm_id", :limit => 2, :offset => 1).sum(:credit_limit) assert_equal [2, 6], c.keys.compact end @@ -150,7 +159,7 @@ class CalculationsTest < ActiveRecord::TestCase end def test_should_group_by_summed_field_having_condition - c = Account.scoped(:group => :firm_id, + c = Account.all.merge!(:group => :firm_id, :having => 'sum(credit_limit) > 50').sum(:credit_limit) assert_nil c[1] assert_equal 105, c[6] @@ -186,7 +195,7 @@ class CalculationsTest < ActiveRecord::TestCase end def test_should_group_by_summed_field_with_conditions - c = Account.scoped(:where => 'firm_id > 1', + c = Account.all.merge!(:where => 'firm_id > 1', :group => :firm_id).sum(:credit_limit) assert_nil c[1] assert_equal 105, c[6] @@ -194,7 +203,7 @@ class CalculationsTest < ActiveRecord::TestCase end def test_should_group_by_summed_field_with_conditions_and_having - c = Account.scoped(:where => 'firm_id > 1', + c = Account.all.merge!(:where => 'firm_id > 1', :group => :firm_id, :having => 'sum(credit_limit) > 60').sum(:credit_limit) assert_nil c[1] @@ -317,19 +326,19 @@ class CalculationsTest < ActiveRecord::TestCase def test_should_count_scoped_select Account.update_all("credit_limit = NULL") - assert_equal 0, Account.scoped(:select => "credit_limit").count + assert_equal 0, Account.all.merge!(:select => "credit_limit").count end def test_should_count_scoped_select_with_options Account.update_all("credit_limit = NULL") - Account.last.update_column('credit_limit', 49) - Account.first.update_column('credit_limit', 51) + Account.last.update_columns('credit_limit' => 49) + Account.first.update_columns('credit_limit' => 51) - assert_equal 1, Account.scoped(:select => "credit_limit").where('credit_limit >= 50').count + assert_equal 1, Account.all.merge!(:select => "credit_limit").where('credit_limit >= 50').count end def test_should_count_manual_select_with_include - assert_equal 6, Account.scoped(:select => "DISTINCT accounts.id", :includes => :firm).count + assert_equal 6, Account.all.merge!(:select => "DISTINCT accounts.id", :includes => :firm).count end def test_count_with_column_parameter @@ -346,7 +355,7 @@ class CalculationsTest < ActiveRecord::TestCase end def test_should_count_field_in_joined_table_with_group_by - c = Account.scoped(:group => 'accounts.firm_id', :joins => :firm).count('companies.id') + c = Account.all.merge!(:group => 'accounts.firm_id', :joins => :firm).count('companies.id') [1,6,2,9].each { |firm_id| assert c.keys.include?(firm_id) } end @@ -420,6 +429,40 @@ class CalculationsTest < ActiveRecord::TestCase Account.where("credit_limit > 50").from('accounts').maximum(:credit_limit) end + def test_maximum_with_not_auto_table_name_prefix_if_column_included + Company.create!(:name => "test", :contracts => [Contract.new(:developer_id => 7)]) + + # TODO: Investigate why PG isn't being typecast + if current_adapter?(:PostgreSQLAdapter) || current_adapter?(:MysqlAdapter) + assert_equal "7", Company.includes(:contracts).maximum(:developer_id) + else + assert_equal 7, Company.includes(:contracts).maximum(:developer_id) + end + end + + def test_minimum_with_not_auto_table_name_prefix_if_column_included + Company.create!(:name => "test", :contracts => [Contract.new(:developer_id => 7)]) + + # TODO: Investigate why PG isn't being typecast + if current_adapter?(:PostgreSQLAdapter) || current_adapter?(:MysqlAdapter) + assert_equal "7", Company.includes(:contracts).minimum(:developer_id) + else + assert_equal 7, Company.includes(:contracts).minimum(:developer_id) + end + end + + def test_sum_with_not_auto_table_name_prefix_if_column_included + Company.create!(:name => "test", :contracts => [Contract.new(:developer_id => 7)]) + + # TODO: Investigate why PG isn't being typecast + if current_adapter?(:MysqlAdapter) || current_adapter?(:PostgreSQLAdapter) + assert_equal "7", Company.includes(:contracts).sum(:developer_id) + else + assert_equal 7, Company.includes(:contracts).sum(:developer_id) + end + end + + def test_from_option_with_specified_index if Edge.connection.adapter_name == 'MySQL' or Edge.connection.adapter_name == 'Mysql2' assert_equal Edge.count(:all), Edge.from('edges USE INDEX(unique_edge_index)').count(:all) @@ -477,6 +520,11 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal [c.id], Company.joins(:contracts).pluck(:id) end + def test_pluck_if_table_included + c = Company.create!(:name => "test", :contracts => [Contract.new(:developer_id => 7)]) + assert_equal [c.id], Company.includes(:contracts).where("contracts.id" => c.contracts.first).pluck(:id) + end + def test_pluck_not_auto_table_name_prefix_if_column_joined Company.create!(:name => "test", :contracts => [Contract.new(:developer_id => 7)]) assert_equal [7], Company.joins(:contracts).pluck(:developer_id) @@ -493,11 +541,39 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal [50 + 53 + 55 + 60], Account.pluck('SUM(DISTINCT(credit_limit)) as credit_limit') end - def test_pluck_expects_a_single_selection - assert_raise(ArgumentError) { Account.pluck 'id, credit_limit' } - end - def test_plucks_with_ids assert_equal Company.all.map(&:id).sort, Company.ids.sort end + + def test_pluck_not_auto_table_name_prefix_if_column_included + Company.create!(:name => "test", :contracts => [Contract.new(:developer_id => 7)]) + ids = Company.includes(:contracts).pluck(:developer_id) + assert_equal Company.count, ids.length + assert_equal [7], ids.compact + end + + def test_pluck_multiple_columns + assert_equal [ + [1, "The First Topic"], [2, "The Second Topic of the day"], + [3, "The Third Topic of the day"], [4, "The Fourth Topic of the day"] + ], Topic.order(:id).pluck(:id, :title) + assert_equal [ + [1, "The First Topic", "David"], [2, "The Second Topic of the day", "Mary"], + [3, "The Third Topic of the day", "Carl"], [4, "The Fourth Topic of the day", "Carl"] + ], Topic.order(:id).pluck(:id, :title, :author_name) + end + + def test_pluck_with_multiple_columns_and_selection_clause + assert_equal [[1, 50], [2, 50], [3, 50], [4, 60], [5, 55], [6, 53]], + Account.pluck('id, credit_limit') + end + + def test_pluck_with_multiple_columns_and_includes + Company.create!(:name => "test", :contracts => [Contract.new(:developer_id => 7)]) + companies_and_developers = Company.order('companies.id').includes(:contracts).pluck(:name, :developer_id) + + assert_equal Company.count, companies_and_developers.length + assert_equal ["37signals", nil], companies_and_developers.first + assert_equal ["test", 7], companies_and_developers.last + end end diff --git a/activerecord/test/cases/callbacks_test.rb b/activerecord/test/cases/callbacks_test.rb index 7690769226..deeef3a3fd 100644 --- a/activerecord/test/cases/callbacks_test.rb +++ b/activerecord/test/cases/callbacks_test.rb @@ -426,11 +426,13 @@ class CallbacksTest < ActiveRecord::TestCase def test_before_destroy_returning_false david = ImmutableDeveloper.find(1) assert !david.destroy + assert_raise(ActiveRecord::RecordNotDestroyed) { david.destroy! } assert_not_nil ImmutableDeveloper.find_by_id(1) someone = CallbackCancellationDeveloper.find(1) someone.cancel_before_destroy = true assert !someone.destroy + assert_raise(ActiveRecord::RecordNotDestroyed) { someone.destroy! } assert !someone.after_destroy_called end diff --git a/activerecord/test/cases/column_definition_test.rb b/activerecord/test/cases/column_definition_test.rb index a44b49466f..bd2fbaa7db 100644 --- a/activerecord/test/cases/column_definition_test.rb +++ b/activerecord/test/cases/column_definition_test.rb @@ -136,12 +136,6 @@ module ActiveRecord smallint_column = PostgreSQLColumn.new('number', nil, oid, "smallint") assert_equal :integer, smallint_column.type end - - def test_uuid_column_should_map_to_string - oid = PostgreSQLAdapter::OID::Identity.new - uuid_column = PostgreSQLColumn.new('unique_id', nil, oid, "uuid") - assert_equal :string, uuid_column.type - end end end end diff --git a/activerecord/test/cases/column_test.rb b/activerecord/test/cases/column_test.rb index 4111a5f808..a7b63d15c9 100644 --- a/activerecord/test/cases/column_test.rb +++ b/activerecord/test/cases/column_test.rb @@ -76,6 +76,12 @@ module ActiveRecord date_string = Time.now.utc.strftime("%F") assert_equal date_string, column.type_cast(date_string).strftime("%F") end + + def test_type_cast_duration_to_integer + column = Column.new("field", nil, "integer") + assert_equal 1800, column.type_cast(30.minutes) + assert_equal 7200, column.type_cast(2.hours) + end end end end diff --git a/activerecord/test/cases/connection_adapters/abstract_adapter_test.rb b/activerecord/test/cases/connection_adapters/abstract_adapter_test.rb index 7dc6e8afcb..3e3d6e2769 100644 --- a/activerecord/test/cases/connection_adapters/abstract_adapter_test.rb +++ b/activerecord/test/cases/connection_adapters/abstract_adapter_test.rb @@ -36,7 +36,7 @@ module ActiveRecord def test_close pool = ConnectionPool.new(ConnectionSpecification.new({}, nil)) - pool.connections << adapter + pool.insert_connection_for_test! adapter adapter.pool = pool # Make sure the pool marks the connection in use diff --git a/activerecord/test/cases/connection_adapters/connection_handler_test.rb b/activerecord/test/cases/connection_adapters/connection_handler_test.rb index dc99ac665c..17cb447105 100644 --- a/activerecord/test/cases/connection_adapters/connection_handler_test.rb +++ b/activerecord/test/cases/connection_adapters/connection_handler_test.rb @@ -7,12 +7,11 @@ module ActiveRecord @handler = ConnectionHandler.new @handler.establish_connection 'america', Base.connection_pool.spec @klass = Class.new do + include Model::Tag def self.name; 'america'; end - class << self - alias active_record_super superclass - end end @subklass = Class.new(@klass) do + include Model::Tag def self.name; 'north america'; end end end diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb index bba7815d73..8287b35aaf 100644 --- a/activerecord/test/cases/connection_pool_test.rb +++ b/activerecord/test/cases/connection_pool_test.rb @@ -200,6 +200,110 @@ module ActiveRecord end.join end + # The connection pool is "fair" if threads waiting for + # connections receive them the order in which they began + # waiting. This ensures that we don't timeout one HTTP request + # even while well under capacity in a multi-threaded environment + # such as a Java servlet container. + # + # We don't need strict fairness: if two connections become + # available at the same time, it's fine of two threads that were + # waiting acquire the connections out of order. + # + # Thus this test prepares waiting threads and then trickles in + # available connections slowly, ensuring the wakeup order is + # correct in this case. + def test_checkout_fairness + @pool.instance_variable_set(:@size, 10) + expected = (1..@pool.size).to_a.freeze + # check out all connections so our threads start out waiting + conns = expected.map { @pool.checkout } + mutex = Mutex.new + order = [] + errors = [] + + threads = expected.map do |i| + t = Thread.new { + begin + @pool.checkout # never checked back in + mutex.synchronize { order << i } + rescue => e + mutex.synchronize { errors << e } + end + } + Thread.pass until t.status == "sleep" + t + end + + # this should wake up the waiting threads one by one in order + conns.each { |conn| @pool.checkin(conn); sleep 0.1 } + + threads.each(&:join) + + raise errors.first if errors.any? + + assert_equal(expected, order) + end + + # As mentioned in #test_checkout_fairness, we don't care about + # strict fairness. This test creates two groups of threads: + # group1 whose members all start waiting before any thread in + # group2. Enough connections are checked in to wakeup all + # group1 threads, and the fact that only group1 and no group2 + # threads acquired a connection is enforced. + def test_checkout_fairness_by_group + @pool.instance_variable_set(:@size, 10) + # take all the connections + conns = (1..10).map { @pool.checkout } + mutex = Mutex.new + successes = [] # threads that successfully got a connection + errors = [] + + make_thread = proc do |i| + t = Thread.new { + begin + @pool.checkout # never checked back in + mutex.synchronize { successes << i } + rescue => e + mutex.synchronize { errors << e } + end + } + Thread.pass until t.status == "sleep" + t + end + + # all group1 threads start waiting before any in group2 + group1 = (1..5).map(&make_thread) + group2 = (6..10).map(&make_thread) + + # checkin n connections back to the pool + checkin = proc do |n| + n.times do + c = conns.pop + @pool.checkin(c) + end + end + + checkin.call(group1.size) # should wake up all group1 + + loop do + sleep 0.1 + break if mutex.synchronize { (successes.size + errors.size) == group1.size } + end + + winners = mutex.synchronize { successes.dup } + checkin.call(group2.size) # should wake up everyone remaining + + group1.each(&:join) + group2.each(&:join) + + assert_equal((1..group1.size).to_a, winners.sort) + + if errors.any? + raise errors.first + end + end + def test_automatic_reconnect= pool = ConnectionPool.new ActiveRecord::Base.connection_pool.spec assert pool.automatic_reconnect diff --git a/activerecord/test/cases/connection_specification/resolver_test.rb b/activerecord/test/cases/connection_specification/resolver_test.rb index e6cb1b9521..673a2b2b88 100644 --- a/activerecord/test/cases/connection_specification/resolver_test.rb +++ b/activerecord/test/cases/connection_specification/resolver_test.rb @@ -9,6 +9,7 @@ module ActiveRecord end def test_url_host_no_db + skip "only if mysql is available" unless defined?(MysqlAdapter) spec = resolve 'mysql://foo?encoding=utf8' assert_equal({ :adapter => "mysql", @@ -18,6 +19,7 @@ module ActiveRecord end def test_url_host_db + skip "only if mysql is available" unless defined?(MysqlAdapter) spec = resolve 'mysql://foo/bar?encoding=utf8' assert_equal({ :adapter => "mysql", @@ -27,6 +29,7 @@ module ActiveRecord end def test_url_port + skip "only if mysql is available" unless defined?(MysqlAdapter) spec = resolve 'mysql://foo:123?encoding=utf8' assert_equal({ :adapter => "mysql", diff --git a/activerecord/test/cases/counter_cache_test.rb b/activerecord/test/cases/counter_cache_test.rb index cd3d19e783..ee443741ca 100644 --- a/activerecord/test/cases/counter_cache_test.rb +++ b/activerecord/test/cases/counter_cache_test.rb @@ -8,9 +8,11 @@ require 'models/category' require 'models/categorization' require 'models/dog' require 'models/dog_lover' +require 'models/person' +require 'models/friendship' class CounterCacheTest < ActiveRecord::TestCase - fixtures :topics, :categories, :categorizations, :cars, :dogs, :dog_lovers + fixtures :topics, :categories, :categorizations, :cars, :dogs, :dog_lovers, :people, :friendships class ::SpecialTopic < ::Topic has_many :special_replies, :foreign_key => 'parent_id' @@ -109,4 +111,11 @@ class CounterCacheTest < ActiveRecord::TestCase Topic.update_counters([t1.id, t2.id], :replies_count => 2) end end + + test "reset the right counter if two have the same foreign key" do + michael = people(:michael) + assert_nothing_raised(ActiveRecord::StatementInvalid) do + Person.reset_counters(michael.id, :followers) + end + end end diff --git a/activerecord/test/cases/custom_locking_test.rb b/activerecord/test/cases/custom_locking_test.rb index 42ef51ef3e..e8290297e3 100644 --- a/activerecord/test/cases/custom_locking_test.rb +++ b/activerecord/test/cases/custom_locking_test.rb @@ -9,7 +9,7 @@ module ActiveRecord if current_adapter?(:MysqlAdapter, :Mysql2Adapter) assert_match 'SHARE MODE', Person.lock('LOCK IN SHARE MODE').to_sql assert_sql(/LOCK IN SHARE MODE/) do - Person.scoped(:lock => 'LOCK IN SHARE MODE').find(1) + Person.all.merge!(:lock => 'LOCK IN SHARE MODE').find(1) end end end diff --git a/activerecord/test/cases/defaults_test.rb b/activerecord/test/cases/defaults_test.rb index b3a281d960..deaf5252db 100644 --- a/activerecord/test/cases/defaults_test.rb +++ b/activerecord/test/cases/defaults_test.rb @@ -1,5 +1,4 @@ require "cases/helper" -require 'active_support/core_ext/object/inclusion' require 'models/default' require 'models/entrant' @@ -95,7 +94,7 @@ if current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter) assert_equal 0, klass.columns_hash['zero'].default assert !klass.columns_hash['zero'].null # 0 in MySQL 4, nil in 5. - assert klass.columns_hash['omit'].default.in?([0, nil]) + assert [0, nil].include?(klass.columns_hash['omit'].default) assert !klass.columns_hash['omit'].null assert_raise(ActiveRecord::StatementInvalid) { klass.create! } diff --git a/activerecord/test/cases/deprecated_dynamic_methods_test.rb b/activerecord/test/cases/deprecated_dynamic_methods_test.rb index 77a609f49a..392f5f4cd5 100644 --- a/activerecord/test/cases/deprecated_dynamic_methods_test.rb +++ b/activerecord/test/cases/deprecated_dynamic_methods_test.rb @@ -1,4 +1,4 @@ -# This file should be deleted when active_record_deprecated_finders is removed as +# This file should be deleted when activerecord-deprecated_finders is removed as # a dependency. # # It is kept for now as there is some fairly nuanced behaviour in the dynamic @@ -336,21 +336,21 @@ class DeprecatedDynamicMethodsTest < ActiveRecord::TestCase def test_find_last_with_limit_gives_same_result_when_loaded_and_unloaded scope = Topic.limit(2) unloaded_last = scope.last - loaded_last = scope.all.last + loaded_last = scope.to_a.last assert_equal loaded_last, unloaded_last end def test_find_last_with_limit_and_offset_gives_same_result_when_loaded_and_unloaded scope = Topic.offset(2).limit(2) unloaded_last = scope.last - loaded_last = scope.all.last + loaded_last = scope.to_a.last assert_equal loaded_last, unloaded_last end def test_find_last_with_offset_gives_same_result_when_loaded_and_unloaded scope = Topic.offset(3) unloaded_last = scope.last - loaded_last = scope.all.last + loaded_last = scope.to_a.last assert_equal loaded_last, unloaded_last end @@ -368,17 +368,17 @@ class DeprecatedDynamicMethodsTest < ActiveRecord::TestCase end def test_dynamic_find_all_should_respect_association_order - assert_equal [companies(:second_client), companies(:first_client)], companies(:first_firm).clients_sorted_desc.scoped(:where => "type = 'Client'").all + assert_equal [companies(:second_client), companies(:first_client)], companies(:first_firm).clients_sorted_desc.where("type = 'Client'").to_a assert_equal [companies(:second_client), companies(:first_client)], companies(:first_firm).clients_sorted_desc.find_all_by_type('Client') end def test_dynamic_find_all_should_respect_association_limit - assert_equal 1, companies(:first_firm).limited_clients.scoped(:where => "type = 'Client'").all.length + assert_equal 1, companies(:first_firm).limited_clients.where("type = 'Client'").to_a.length assert_equal 1, companies(:first_firm).limited_clients.find_all_by_type('Client').length end def test_dynamic_find_all_limit_should_override_association_limit - assert_equal 2, companies(:first_firm).limited_clients.scoped(:where => "type = 'Client'", :limit => 9_000).all.length + assert_equal 2, companies(:first_firm).limited_clients.where("type = 'Client'").limit(9_000).to_a.length assert_equal 2, companies(:first_firm).limited_clients.find_all_by_type('Client', :limit => 9_000).length end @@ -396,22 +396,22 @@ class DeprecatedDynamicMethodsTest < ActiveRecord::TestCase end def test_dynamic_find_all_should_respect_association_order_for_through - assert_equal [Comment.find(10), Comment.find(7), Comment.find(6), Comment.find(3)], authors(:david).comments_desc.scoped(:where => "comments.type = 'SpecialComment'").all + assert_equal [Comment.find(10), Comment.find(7), Comment.find(6), Comment.find(3)], authors(:david).comments_desc.where("comments.type = 'SpecialComment'").to_a assert_equal [Comment.find(10), Comment.find(7), Comment.find(6), Comment.find(3)], authors(:david).comments_desc.find_all_by_type('SpecialComment') end def test_dynamic_find_all_should_respect_association_limit_for_through - assert_equal 1, authors(:david).limited_comments.scoped(:where => "comments.type = 'SpecialComment'").all.length + assert_equal 1, authors(:david).limited_comments.where("comments.type = 'SpecialComment'").to_a.length assert_equal 1, authors(:david).limited_comments.find_all_by_type('SpecialComment').length end def test_dynamic_find_all_order_should_override_association_limit_for_through - assert_equal 4, authors(:david).limited_comments.scoped(:where => "comments.type = 'SpecialComment'", :limit => 9_000).all.length + assert_equal 4, authors(:david).limited_comments.where("comments.type = 'SpecialComment'").limit(9_000).to_a.length assert_equal 4, authors(:david).limited_comments.find_all_by_type('SpecialComment', :limit => 9_000).length end def test_find_all_include_over_the_same_table_for_through - assert_equal 2, people(:michael).posts.scoped(:includes => :people).all.length + assert_equal 2, people(:michael).posts.includes(:people).to_a.length end def test_find_or_create_by_resets_cached_counters @@ -487,7 +487,7 @@ class DeprecatedDynamicMethodsTest < ActiveRecord::TestCase end def test_dynamic_find_all_by_attributes - authors = Author.scoped + authors = Author.all davids = authors.find_all_by_name('David') assert_kind_of Array, davids @@ -495,7 +495,7 @@ class DeprecatedDynamicMethodsTest < ActiveRecord::TestCase end def test_dynamic_find_or_initialize_by_attributes - authors = Author.scoped + authors = Author.all lifo = authors.find_or_initialize_by_name('Lifo') assert_equal "Lifo", lifo.name @@ -505,7 +505,7 @@ class DeprecatedDynamicMethodsTest < ActiveRecord::TestCase end def test_dynamic_find_or_create_by_attributes - authors = Author.scoped + authors = Author.all lifo = authors.find_or_create_by_name('Lifo') assert_equal "Lifo", lifo.name @@ -515,7 +515,7 @@ class DeprecatedDynamicMethodsTest < ActiveRecord::TestCase end def test_dynamic_find_or_create_by_attributes_bang - authors = Author.scoped + authors = Author.all assert_raises(ActiveRecord::RecordInvalid) { authors.find_or_create_by_name!('') } @@ -580,7 +580,7 @@ class DynamicScopeTest < ActiveRecord::TestCase def test_dynamic_scope assert_equal @test_klass.scoped_by_author_id(1).find(1), @test_klass.find(1) - assert_equal @test_klass.scoped_by_author_id_and_title(1, "Welcome to the weblog").first, @test_klass.scoped(:where => { :author_id => 1, :title => "Welcome to the weblog"}).first + assert_equal @test_klass.scoped_by_author_id_and_title(1, "Welcome to the weblog").first, @test_klass.all.merge!(:where => { :author_id => 1, :title => "Welcome to the weblog"}).first end def test_dynamic_scope_should_create_methods_after_hitting_method_missing @@ -600,7 +600,8 @@ class DynamicScopeMatchTest < ActiveRecord::TestCase end def test_scoped_by - match = ActiveRecord::DynamicMatchers::Method.match(nil, "scoped_by_age_and_sex_and_location") + model = stub(attribute_aliases: {}) + match = ActiveRecord::DynamicMatchers::Method.match(model, "scoped_by_age_and_sex_and_location") assert_not_nil match assert_equal %w(age sex location), match.attribute_names end diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index 2650040a80..92677b9926 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -79,6 +79,17 @@ class DirtyTest < ActiveRecord::TestCase end end + def test_setting_time_attributes_with_time_zone_field_to_itself_should_not_be_marked_as_a_change + in_time_zone 'Paris' do + target = Class.new(ActiveRecord::Base) + target.table_name = 'pirates' + + pirate = target.create + pirate.created_on = pirate.created_on + assert !pirate.created_on_changed? + end + end + def test_time_attributes_changes_without_time_zone_by_skip in_time_zone 'Paris' do target = Class.new(ActiveRecord::Base) @@ -190,7 +201,7 @@ class DirtyTest < ActiveRecord::TestCase end end - def test_nullable_integer_zero_to_string_zero_not_marked_as_changed + def test_integer_zero_to_string_zero_not_marked_as_changed pirate = Pirate.new pirate.parrot_id = 0 pirate.catchphrase = 'arrr' @@ -202,6 +213,19 @@ class DirtyTest < ActiveRecord::TestCase assert !pirate.changed? end + def test_integer_zero_to_integer_zero_not_marked_as_changed + pirate = Pirate.new + pirate.parrot_id = 0 + pirate.catchphrase = 'arrr' + assert pirate.save! + + assert !pirate.changed? + + pirate.parrot_id = 0 + assert !pirate.changed? + end + + def test_zero_to_blank_marked_as_changed pirate = Pirate.new pirate.catchphrase = "Yarrrr, me hearties" @@ -413,7 +437,7 @@ class DirtyTest < ActiveRecord::TestCase with_partial_updates(Topic) do Topic.create!(:author_name => 'Bill', :content => {:a => "a"}) topic = Topic.select('id, author_name').first - topic.update_column :author_name, 'John' + topic.update_columns author_name: 'John' topic = Topic.first assert_not_nil topic.content end @@ -485,16 +509,6 @@ class DirtyTest < ActiveRecord::TestCase assert_not_nil pirate.previous_changes['updated_on'][1] assert !pirate.previous_changes.key?('parrot_id') assert !pirate.previous_changes.key?('created_on') - - pirate = Pirate.find_by_catchphrase("Ahoy!") - pirate.update_attribute(:catchphrase, "Ninjas suck!") - - assert_equal 2, pirate.previous_changes.size - assert_equal ["Ahoy!", "Ninjas suck!"], pirate.previous_changes['catchphrase'] - assert_not_nil pirate.previous_changes['updated_on'][0] - assert_not_nil pirate.previous_changes['updated_on'][1] - assert !pirate.previous_changes.key?('parrot_id') - assert !pirate.previous_changes.key?('created_on') end if ActiveRecord::Base.connection.supports_migrations? @@ -511,6 +525,21 @@ class DirtyTest < ActiveRecord::TestCase end end + def test_setting_time_attributes_with_time_zone_field_to_same_time_should_not_be_marked_as_a_change + in_time_zone 'Paris' do + target = Class.new(ActiveRecord::Base) + target.table_name = 'pirates' + + created_on = Time.now + + pirate = target.create(:created_on => created_on) + pirate.reload # Here mysql truncate the usec value to 0 + + pirate.created_on = created_on + assert !pirate.created_on_changed? + end + end + private def with_partial_updates(klass, on = true) old = klass.partial_updates? diff --git a/activerecord/test/cases/explain_subscriber_test.rb b/activerecord/test/cases/explain_subscriber_test.rb index e118add44c..91e1df91cd 100644 --- a/activerecord/test/cases/explain_subscriber_test.rb +++ b/activerecord/test/cases/explain_subscriber_test.rb @@ -6,14 +6,14 @@ if ActiveRecord::Base.connection.supports_explain? def test_collects_nothing_if_available_queries_for_explain_is_nil with_queries(nil) do - SUBSCRIBER.call + SUBSCRIBER.finish(nil, nil, {}) assert_nil Thread.current[:available_queries_for_explain] end end def test_collects_nothing_if_the_payload_has_an_exception with_queries([]) do |queries| - SUBSCRIBER.call(:exception => Exception.new) + SUBSCRIBER.finish(nil, nil, :exception => Exception.new) assert queries.empty? end end @@ -21,7 +21,7 @@ if ActiveRecord::Base.connection.supports_explain? def test_collects_nothing_for_ignored_payloads with_queries([]) do |queries| ActiveRecord::ExplainSubscriber::IGNORED_PAYLOADS.each do |ip| - SUBSCRIBER.call(:name => ip) + SUBSCRIBER.finish(nil, nil, :name => ip) end assert queries.empty? end @@ -31,7 +31,7 @@ if ActiveRecord::Base.connection.supports_explain? sql = 'select 1 from users' binds = [1, 2] with_queries([]) do |queries| - SUBSCRIBER.call(:name => 'SQL', :sql => sql, :binds => binds) + SUBSCRIBER.finish(nil, nil, :name => 'SQL', :sql => sql, :binds => binds) assert_equal 1, queries.size assert_equal sql, queries[0][0] assert_equal binds, queries[0][1] @@ -45,4 +45,4 @@ if ActiveRecord::Base.connection.supports_explain? Thread.current[:available_queries_for_explain] = nil end end -end
\ No newline at end of file +end diff --git a/activerecord/test/cases/explain_test.rb b/activerecord/test/cases/explain_test.rb index cb7781f8e7..6dce8ccdd1 100644 --- a/activerecord/test/cases/explain_test.rb +++ b/activerecord/test/cases/explain_test.rb @@ -20,7 +20,7 @@ if ActiveRecord::Base.connection.supports_explain? end with_threshold(0) do - Car.where(:name => 'honda').all + Car.where(:name => 'honda').to_a end end @@ -45,7 +45,7 @@ if ActiveRecord::Base.connection.supports_explain? queries = Thread.current[:available_queries_for_explain] = [] with_threshold(0) do - Car.where(:name => 'honda').all + Car.where(:name => 'honda').to_a end sql, binds = queries[0] @@ -58,7 +58,7 @@ if ActiveRecord::Base.connection.supports_explain? def test_collecting_queries_for_explain result, queries = ActiveRecord::Base.collecting_queries_for_explain do - Car.where(:name => 'honda').all + Car.where(:name => 'honda').to_a end sql, binds = queries[0] @@ -68,6 +68,16 @@ if ActiveRecord::Base.connection.supports_explain? assert_equal [cars(:honda)], result end + def test_logging_query_plan_when_counting_by_sql + base.logger.expects(:warn).with do |message| + message.starts_with?('EXPLAIN for:') + end + + with_threshold(0) do + Car.count_by_sql "SELECT COUNT(*) FROM cars WHERE name = 'honda'" + end + end + def test_exec_explain_with_no_binds sqls = %w(foo bar) binds = [[], []] diff --git a/activerecord/test/cases/finder_respond_to_test.rb b/activerecord/test/cases/finder_respond_to_test.rb index 810c1500cc..9440cd429a 100644 --- a/activerecord/test/cases/finder_respond_to_test.rb +++ b/activerecord/test/cases/finder_respond_to_test.rb @@ -36,6 +36,11 @@ class FinderRespondToTest < ActiveRecord::TestCase assert_respond_to Topic, :find_by_title_and_author_name end + def test_should_respond_to_find_all_by_an_aliased_attribute + ensure_topic_method_is_not_cached(:find_by_heading) + assert_respond_to Topic, :find_by_heading + end + def test_should_respond_to_find_or_initialize_from_one_attribute ensure_topic_method_is_not_cached(:find_or_initialize_by_title) assert_respond_to Topic, :find_or_initialize_by_title @@ -80,7 +85,6 @@ class FinderRespondToTest < ActiveRecord::TestCase private def ensure_topic_method_is_not_cached(method_id) - class << Topic; self; end.send(:remove_method, method_id) if Topic.public_methods.any? { |m| m.to_s == method_id.to_s } + class << Topic; self; end.send(:remove_method, method_id) if Topic.public_methods.include? method_id end - end diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index f7ecab28ce..20c8e8894d 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -45,10 +45,20 @@ class FinderTest < ActiveRecord::TestCase assert_raise(NoMethodError) { Topic.exists?([1,2]) } end + def test_exists_does_not_select_columns_without_alias + assert_sql(/SELECT\W+1 AS one FROM ["`]topics["`]/i) do + Topic.exists? + end + end + def test_exists_returns_true_with_one_record_and_no_args assert Topic.exists? end + def test_exists_returns_false_with_false_arg + assert !Topic.exists?(false) + end + # exists? should handle nil for id's that come from URLs and always return false # (example: Topic.exists?(params[:id])) where params[:id] is nil def test_exists_with_nil_arg @@ -63,7 +73,12 @@ class FinderTest < ActiveRecord::TestCase assert Topic.order(:id).uniq.exists? end - def test_does_not_exist_with_empty_table_and_no_args_given + def test_exists_with_includes_limit_and_empty_result + assert !Topic.includes(:replies).limit(0).exists? + assert !Topic.includes(:replies).limit(1).where('0 = 1').exists? + end + + def test_exists_with_empty_table_and_no_args_given Topic.delete_all assert !Topic.exists? end @@ -99,14 +114,14 @@ class FinderTest < ActiveRecord::TestCase end def test_find_by_ids_with_limit_and_offset - assert_equal 2, Entrant.scoped(:limit => 2).find([1,3,2]).size - assert_equal 1, Entrant.scoped(:limit => 3, :offset => 2).find([1,3,2]).size + assert_equal 2, Entrant.all.merge!(:limit => 2).find([1,3,2]).size + assert_equal 1, Entrant.all.merge!(:limit => 3, :offset => 2).find([1,3,2]).size # Also test an edge case: If you have 11 results, and you set a # limit of 3 and offset of 9, then you should find that there # will be only 2 results, regardless of the limit. devs = Developer.all - last_devs = Developer.scoped(:limit => 3, :offset => 9).find devs.map(&:id) + last_devs = Developer.all.merge!(:limit => 3, :offset => 9).find devs.map(&:id) assert_equal 2, last_devs.size end @@ -123,7 +138,7 @@ class FinderTest < ActiveRecord::TestCase end def test_find_with_group_and_sanitized_having_method - developers = Developer.group(:salary).having("sum(salary) > ?", 10000).select('salary').all + developers = Developer.group(:salary).having("sum(salary) > ?", 10000).select('salary').to_a assert_equal 3, developers.size assert_equal 3, developers.map(&:salary).uniq.size assert developers.all? { |developer| developer.salary > 10000 } @@ -259,7 +274,7 @@ class FinderTest < ActiveRecord::TestCase end def test_find_only_some_columns - topic = Topic.scoped(:select => "author_name").find(1) + topic = Topic.all.merge!(:select => "author_name").find(1) assert_raise(ActiveModel::MissingAttributeError) {topic.title} assert_nil topic.read_attribute("title") assert_equal "David", topic.author_name @@ -270,23 +285,23 @@ class FinderTest < ActiveRecord::TestCase end def test_find_on_array_conditions - assert Topic.scoped(:where => ["approved = ?", false]).find(1) - assert_raise(ActiveRecord::RecordNotFound) { Topic.scoped(:where => ["approved = ?", true]).find(1) } + assert Topic.all.merge!(:where => ["approved = ?", false]).find(1) + assert_raise(ActiveRecord::RecordNotFound) { Topic.all.merge!(:where => ["approved = ?", true]).find(1) } end def test_find_on_hash_conditions - assert Topic.scoped(:where => { :approved => false }).find(1) - assert_raise(ActiveRecord::RecordNotFound) { Topic.scoped(:where => { :approved => true }).find(1) } + assert Topic.all.merge!(:where => { :approved => false }).find(1) + assert_raise(ActiveRecord::RecordNotFound) { Topic.all.merge!(:where => { :approved => true }).find(1) } end def test_find_on_hash_conditions_with_explicit_table_name - assert Topic.scoped(:where => { 'topics.approved' => false }).find(1) - assert_raise(ActiveRecord::RecordNotFound) { Topic.scoped(:where => { 'topics.approved' => true }).find(1) } + assert Topic.all.merge!(:where => { 'topics.approved' => false }).find(1) + assert_raise(ActiveRecord::RecordNotFound) { Topic.all.merge!(:where => { 'topics.approved' => true }).find(1) } end def test_find_on_hash_conditions_with_hashed_table_name - assert Topic.scoped(:where => {:topics => { :approved => false }}).find(1) - assert_raise(ActiveRecord::RecordNotFound) { Topic.scoped(:where => {:topics => { :approved => true }}).find(1) } + assert Topic.all.merge!(:where => {:topics => { :approved => false }}).find(1) + assert_raise(ActiveRecord::RecordNotFound) { Topic.all.merge!(:where => {:topics => { :approved => true }}).find(1) } end def test_find_with_hash_conditions_on_joined_table @@ -296,16 +311,16 @@ class FinderTest < ActiveRecord::TestCase end def test_find_with_hash_conditions_on_joined_table_and_with_range - firms = DependentFirm.scoped :joins => :account, :where => {:name => 'RailsCore', :accounts => { :credit_limit => 55..60 }} + firms = DependentFirm.all.merge!(:joins => :account, :where => {:name => 'RailsCore', :accounts => { :credit_limit => 55..60 }}) assert_equal 1, firms.size assert_equal companies(:rails_core), firms.first end def test_find_on_hash_conditions_with_explicit_table_name_and_aggregate david = customers(:david) - assert Customer.scoped(:where => { 'customers.name' => david.name, :address => david.address }).find(david.id) + assert Customer.where('customers.name' => david.name, :address => david.address).find(david.id) assert_raise(ActiveRecord::RecordNotFound) { - Customer.scoped(:where => { 'customers.name' => david.name + "1", :address => david.address }).find(david.id) + Customer.where('customers.name' => david.name + "1", :address => david.address).find(david.id) } end @@ -314,71 +329,71 @@ class FinderTest < ActiveRecord::TestCase end def test_find_on_hash_conditions_with_range - assert_equal [1,2], Topic.scoped(:where => { :id => 1..2 }).all.map(&:id).sort - assert_raise(ActiveRecord::RecordNotFound) { Topic.scoped(:where => { :id => 2..3 }).find(1) } + assert_equal [1,2], Topic.all.merge!(:where => { :id => 1..2 }).to_a.map(&:id).sort + assert_raise(ActiveRecord::RecordNotFound) { Topic.all.merge!(:where => { :id => 2..3 }).find(1) } end def test_find_on_hash_conditions_with_end_exclusive_range - assert_equal [1,2,3], Topic.scoped(:where => { :id => 1..3 }).all.map(&:id).sort - assert_equal [1,2], Topic.scoped(:where => { :id => 1...3 }).all.map(&:id).sort - assert_raise(ActiveRecord::RecordNotFound) { Topic.scoped(:where => { :id => 2...3 }).find(3) } + assert_equal [1,2,3], Topic.all.merge!(:where => { :id => 1..3 }).to_a.map(&:id).sort + assert_equal [1,2], Topic.all.merge!(:where => { :id => 1...3 }).to_a.map(&:id).sort + assert_raise(ActiveRecord::RecordNotFound) { Topic.all.merge!(:where => { :id => 2...3 }).find(3) } end def test_find_on_hash_conditions_with_multiple_ranges - assert_equal [1,2,3], Comment.scoped(:where => { :id => 1..3, :post_id => 1..2 }).all.map(&:id).sort - assert_equal [1], Comment.scoped(:where => { :id => 1..1, :post_id => 1..10 }).all.map(&:id).sort + assert_equal [1,2,3], Comment.all.merge!(:where => { :id => 1..3, :post_id => 1..2 }).to_a.map(&:id).sort + assert_equal [1], Comment.all.merge!(:where => { :id => 1..1, :post_id => 1..10 }).to_a.map(&:id).sort end def test_find_on_hash_conditions_with_array_of_integers_and_ranges - assert_equal [1,2,3,5,6,7,8,9], Comment.scoped(:where => {:id => [1..2, 3, 5, 6..8, 9]}).all.map(&:id).sort + assert_equal [1,2,3,5,6,7,8,9], Comment.all.merge!(:where => {:id => [1..2, 3, 5, 6..8, 9]}).to_a.map(&:id).sort end def test_find_on_multiple_hash_conditions - assert Topic.scoped(:where => { :author_name => "David", :title => "The First Topic", :replies_count => 1, :approved => false }).find(1) - assert_raise(ActiveRecord::RecordNotFound) { Topic.scoped(:where => { :author_name => "David", :title => "The First Topic", :replies_count => 1, :approved => true }).find(1) } - assert_raise(ActiveRecord::RecordNotFound) { Topic.scoped(:where => { :author_name => "David", :title => "HHC", :replies_count => 1, :approved => false }).find(1) } - assert_raise(ActiveRecord::RecordNotFound) { Topic.scoped(:where => { :author_name => "David", :title => "The First Topic", :replies_count => 1, :approved => true }).find(1) } + assert Topic.all.merge!(:where => { :author_name => "David", :title => "The First Topic", :replies_count => 1, :approved => false }).find(1) + assert_raise(ActiveRecord::RecordNotFound) { Topic.all.merge!(:where => { :author_name => "David", :title => "The First Topic", :replies_count => 1, :approved => true }).find(1) } + assert_raise(ActiveRecord::RecordNotFound) { Topic.all.merge!(:where => { :author_name => "David", :title => "HHC", :replies_count => 1, :approved => false }).find(1) } + assert_raise(ActiveRecord::RecordNotFound) { Topic.all.merge!(:where => { :author_name => "David", :title => "The First Topic", :replies_count => 1, :approved => true }).find(1) } end def test_condition_interpolation assert_kind_of Firm, Company.where("name = '%s'", "37signals").first - assert_nil Company.scoped(:where => ["name = '%s'", "37signals!"]).first - assert_nil Company.scoped(:where => ["name = '%s'", "37signals!' OR 1=1"]).first - assert_kind_of Time, Topic.scoped(:where => ["id = %d", 1]).first.written_on + assert_nil Company.all.merge!(:where => ["name = '%s'", "37signals!"]).first + assert_nil Company.all.merge!(:where => ["name = '%s'", "37signals!' OR 1=1"]).first + assert_kind_of Time, Topic.all.merge!(:where => ["id = %d", 1]).first.written_on end def test_condition_array_interpolation - assert_kind_of Firm, Company.scoped(:where => ["name = '%s'", "37signals"]).first - assert_nil Company.scoped(:where => ["name = '%s'", "37signals!"]).first - assert_nil Company.scoped(:where => ["name = '%s'", "37signals!' OR 1=1"]).first - assert_kind_of Time, Topic.scoped(:where => ["id = %d", 1]).first.written_on + assert_kind_of Firm, Company.all.merge!(:where => ["name = '%s'", "37signals"]).first + assert_nil Company.all.merge!(:where => ["name = '%s'", "37signals!"]).first + assert_nil Company.all.merge!(:where => ["name = '%s'", "37signals!' OR 1=1"]).first + assert_kind_of Time, Topic.all.merge!(:where => ["id = %d", 1]).first.written_on end def test_condition_hash_interpolation - assert_kind_of Firm, Company.scoped(:where => { :name => "37signals"}).first - assert_nil Company.scoped(:where => { :name => "37signals!"}).first - assert_kind_of Time, Topic.scoped(:where => {:id => 1}).first.written_on + assert_kind_of Firm, Company.all.merge!(:where => { :name => "37signals"}).first + assert_nil Company.all.merge!(:where => { :name => "37signals!"}).first + assert_kind_of Time, Topic.all.merge!(:where => {:id => 1}).first.written_on end def test_hash_condition_find_malformed assert_raise(ActiveRecord::StatementInvalid) { - Company.scoped(:where => { :id => 2, :dhh => true }).first + Company.all.merge!(:where => { :id => 2, :dhh => true }).first } end def test_hash_condition_find_with_escaped_characters Company.create("name" => "Ain't noth'n like' \#stuff") - assert Company.scoped(:where => { :name => "Ain't noth'n like' \#stuff" }).first + assert Company.all.merge!(:where => { :name => "Ain't noth'n like' \#stuff" }).first end def test_hash_condition_find_with_array - p1, p2 = Post.scoped(:limit => 2, :order => 'id asc').all - assert_equal [p1, p2], Post.scoped(:where => { :id => [p1, p2] }, :order => 'id asc').all - assert_equal [p1, p2], Post.scoped(:where => { :id => [p1, p2.id] }, :order => 'id asc').all + p1, p2 = Post.all.merge!(:limit => 2, :order => 'id asc').to_a + assert_equal [p1, p2], Post.all.merge!(:where => { :id => [p1, p2] }, :order => 'id asc').to_a + assert_equal [p1, p2], Post.all.merge!(:where => { :id => [p1, p2.id] }, :order => 'id asc').to_a end def test_hash_condition_find_with_nil - topic = Topic.scoped(:where => { :last_read => nil } ).first + topic = Topic.all.merge!(:where => { :last_read => nil } ).first assert_not_nil topic assert_nil topic.last_read end @@ -386,42 +401,42 @@ class FinderTest < ActiveRecord::TestCase def test_hash_condition_find_with_aggregate_having_one_mapping balance = customers(:david).balance assert_kind_of Money, balance - found_customer = Customer.scoped(:where => {:balance => balance}).first + found_customer = Customer.where(:balance => balance).first assert_equal customers(:david), found_customer end def test_hash_condition_find_with_aggregate_attribute_having_same_name_as_field_and_key_value_being_aggregate gps_location = customers(:david).gps_location assert_kind_of GpsLocation, gps_location - found_customer = Customer.scoped(:where => {:gps_location => gps_location}).first + found_customer = Customer.where(:gps_location => gps_location).first assert_equal customers(:david), found_customer end def test_hash_condition_find_with_aggregate_having_one_mapping_and_key_value_being_attribute_value balance = customers(:david).balance assert_kind_of Money, balance - found_customer = Customer.scoped(:where => {:balance => balance.amount}).first + found_customer = Customer.where(:balance => balance.amount).first assert_equal customers(:david), found_customer end def test_hash_condition_find_with_aggregate_attribute_having_same_name_as_field_and_key_value_being_attribute_value gps_location = customers(:david).gps_location assert_kind_of GpsLocation, gps_location - found_customer = Customer.scoped(:where => {:gps_location => gps_location.gps_location}).first + found_customer = Customer.where(:gps_location => gps_location.gps_location).first assert_equal customers(:david), found_customer end def test_hash_condition_find_with_aggregate_having_three_mappings address = customers(:david).address assert_kind_of Address, address - found_customer = Customer.scoped(:where => {:address => address}).first + found_customer = Customer.where(:address => address).first assert_equal customers(:david), found_customer end def test_hash_condition_find_with_one_condition_being_aggregate_and_another_not address = customers(:david).address assert_kind_of Address, address - found_customer = Customer.scoped(:where => {:address => address, :name => customers(:david).name}).first + found_customer = Customer.where(:address => address, :name => customers(:david).name).first assert_equal customers(:david), found_customer end @@ -429,7 +444,7 @@ class FinderTest < ActiveRecord::TestCase with_env_tz 'America/New_York' do with_active_record_default_timezone :local do topic = Topic.first - assert_equal topic, Topic.scoped(:where => ['written_on = ?', topic.written_on.getutc]).first + assert_equal topic, Topic.all.merge!(:where => ['written_on = ?', topic.written_on.getutc]).first end end end @@ -438,7 +453,7 @@ class FinderTest < ActiveRecord::TestCase with_env_tz 'America/New_York' do with_active_record_default_timezone :local do topic = Topic.first - assert_equal topic, Topic.scoped(:where => {:written_on => topic.written_on.getutc}).first + assert_equal topic, Topic.all.merge!(:where => {:written_on => topic.written_on.getutc}).first end end end @@ -447,7 +462,7 @@ class FinderTest < ActiveRecord::TestCase with_env_tz 'America/New_York' do with_active_record_default_timezone :utc do topic = Topic.first - assert_equal topic, Topic.scoped(:where => ['written_on = ?', topic.written_on.getlocal]).first + assert_equal topic, Topic.all.merge!(:where => ['written_on = ?', topic.written_on.getlocal]).first end end end @@ -456,32 +471,32 @@ class FinderTest < ActiveRecord::TestCase with_env_tz 'America/New_York' do with_active_record_default_timezone :utc do topic = Topic.first - assert_equal topic, Topic.scoped(:where => {:written_on => topic.written_on.getlocal}).first + assert_equal topic, Topic.all.merge!(:where => {:written_on => topic.written_on.getlocal}).first end end end def test_bind_variables - assert_kind_of Firm, Company.scoped(:where => ["name = ?", "37signals"]).first - assert_nil Company.scoped(:where => ["name = ?", "37signals!"]).first - assert_nil Company.scoped(:where => ["name = ?", "37signals!' OR 1=1"]).first - assert_kind_of Time, Topic.scoped(:where => ["id = ?", 1]).first.written_on + assert_kind_of Firm, Company.all.merge!(:where => ["name = ?", "37signals"]).first + assert_nil Company.all.merge!(:where => ["name = ?", "37signals!"]).first + assert_nil Company.all.merge!(:where => ["name = ?", "37signals!' OR 1=1"]).first + assert_kind_of Time, Topic.all.merge!(:where => ["id = ?", 1]).first.written_on assert_raise(ActiveRecord::PreparedStatementInvalid) { - Company.scoped(:where => ["id=? AND name = ?", 2]).first + Company.all.merge!(:where => ["id=? AND name = ?", 2]).first } assert_raise(ActiveRecord::PreparedStatementInvalid) { - Company.scoped(:where => ["id=?", 2, 3, 4]).first + Company.all.merge!(:where => ["id=?", 2, 3, 4]).first } end def test_bind_variables_with_quotes Company.create("name" => "37signals' go'es agains") - assert Company.scoped(:where => ["name = ?", "37signals' go'es agains"]).first + assert Company.all.merge!(:where => ["name = ?", "37signals' go'es agains"]).first end def test_named_bind_variables_with_quotes Company.create("name" => "37signals' go'es agains") - assert Company.scoped(:where => ["name = :name", {:name => "37signals' go'es agains"}]).first + assert Company.all.merge!(:where => ["name = :name", {:name => "37signals' go'es agains"}]).first end def test_bind_arity @@ -499,10 +514,10 @@ class FinderTest < ActiveRecord::TestCase assert_nothing_raised { bind("'+00:00'", :foo => "bar") } - assert_kind_of Firm, Company.scoped(:where => ["name = :name", { :name => "37signals" }]).first - assert_nil Company.scoped(:where => ["name = :name", { :name => "37signals!" }]).first - assert_nil Company.scoped(:where => ["name = :name", { :name => "37signals!' OR 1=1" }]).first - assert_kind_of Time, Topic.scoped(:where => ["id = :id", { :id => 1 }]).first.written_on + assert_kind_of Firm, Company.all.merge!(:where => ["name = :name", { :name => "37signals" }]).first + assert_nil Company.all.merge!(:where => ["name = :name", { :name => "37signals!" }]).first + assert_nil Company.all.merge!(:where => ["name = :name", { :name => "37signals!' OR 1=1" }]).first + assert_kind_of Time, Topic.all.merge!(:where => ["id = :id", { :id => 1 }]).first.written_on end class SimpleEnumerable @@ -589,6 +604,11 @@ class FinderTest < ActiveRecord::TestCase assert_raise(ActiveRecord::RecordNotFound) { Topic.find_by_title!("The First Topic!") } end + def test_find_by_one_attribute_that_is_an_alias + assert_equal topics(:first), Topic.find_by_heading("The First Topic") + assert_nil Topic.find_by_heading("The First Topic!") + end + def test_find_by_one_attribute_with_conditions assert_equal accounts(:rails_core_account), Account.where('firm_id = ?', 6).find_by_credit_limit(50) end @@ -629,7 +649,7 @@ class FinderTest < ActiveRecord::TestCase def test_dynamic_finder_on_one_attribute_with_conditions_returns_same_results_after_caching # ensure this test can run independently of order - class << Account; self; end.send(:remove_method, :find_by_credit_limit) if Account.public_methods.any? { |m| m.to_s == 'find_by_credit_limit' } + class << Account; self; end.send(:remove_method, :find_by_credit_limit) if Account.public_methods.include?(:find_by_credit_limit) a = Account.where('firm_id = ?', 6).find_by_credit_limit(50) assert_equal a, Account.where('firm_id = ?', 6).find_by_credit_limit(50) # find_by_credit_limit has been cached end @@ -674,10 +694,10 @@ class FinderTest < ActiveRecord::TestCase end def test_find_all_with_join - developers_on_project_one = Developer.scoped( + developers_on_project_one = Developer.all.merge!( :joins => 'LEFT JOIN developers_projects ON developers.id = developers_projects.developer_id', :where => 'project_id=1' - ).all + ).to_a assert_equal 3, developers_on_project_one.length developer_names = developers_on_project_one.map { |d| d.name } assert developer_names.include?('David') @@ -685,7 +705,7 @@ class FinderTest < ActiveRecord::TestCase end def test_joins_dont_clobber_id - first = Firm.scoped( + first = Firm.all.merge!( :joins => 'INNER JOIN companies clients ON clients.firm_id = companies.id', :where => 'companies.id = 1' ).first @@ -693,7 +713,7 @@ class FinderTest < ActiveRecord::TestCase end def test_joins_with_string_array - person_with_reader_and_post = Post.scoped( + person_with_reader_and_post = Post.all.merge!( :joins => [ "INNER JOIN categorizations ON categorizations.post_id = posts.id", "INNER JOIN categories ON categories.id = categorizations.category_id AND categories.type = 'SpecialCategory'" @@ -723,9 +743,9 @@ class FinderTest < ActiveRecord::TestCase end def test_find_by_records - p1, p2 = Post.scoped(:limit => 2, :order => 'id asc').all - assert_equal [p1, p2], Post.scoped(:where => ['id in (?)', [p1, p2]], :order => 'id asc') - assert_equal [p1, p2], Post.scoped(:where => ['id in (?)', [p1, p2.id]], :order => 'id asc') + p1, p2 = Post.all.merge!(:limit => 2, :order => 'id asc').to_a + assert_equal [p1, p2], Post.all.merge!(:where => ['id in (?)', [p1, p2]], :order => 'id asc') + assert_equal [p1, p2], Post.all.merge!(:where => ['id in (?)', [p1, p2.id]], :order => 'id asc') end def test_select_value @@ -752,14 +772,14 @@ class FinderTest < ActiveRecord::TestCase end def test_find_with_order_on_included_associations_with_construct_finder_sql_for_association_limiting_and_is_distinct - assert_equal 2, Post.scoped(:includes => { :authors => :author_address }, :order => 'author_addresses.id DESC ', :limit => 2).all.size + assert_equal 2, Post.all.merge!(:includes => { :authors => :author_address }, :order => 'author_addresses.id DESC ', :limit => 2).to_a.size - assert_equal 3, Post.scoped(:includes => { :author => :author_address, :authors => :author_address}, - :order => 'author_addresses_authors.id DESC ', :limit => 3).all.size + assert_equal 3, Post.all.merge!(:includes => { :author => :author_address, :authors => :author_address}, + :order => 'author_addresses_authors.id DESC ', :limit => 3).to_a.size end def test_find_with_nil_inside_set_passed_for_one_attribute - client_of = Company.scoped( + client_of = Company.all.merge!( :where => { :client_of => [2, 1, nil], :name => ['37signals', 'Summit', 'Microsoft'] }, @@ -771,7 +791,7 @@ class FinderTest < ActiveRecord::TestCase end def test_find_with_nil_inside_set_passed_for_attribute - client_of = Company.scoped( + client_of = Company.all.merge!( :where => { :client_of => [nil] }, :order => 'client_of DESC' ).map { |x| x.client_of } @@ -780,10 +800,10 @@ class FinderTest < ActiveRecord::TestCase end def test_with_limiting_with_custom_select - posts = Post.references(:authors).scoped( + posts = Post.references(:authors).merge( :includes => :author, :select => ' posts.*, authors.id as "author_id"', :limit => 3, :order => 'posts.id' - ).all + ).to_a assert_equal 3, posts.size assert_equal [0, 1, 1], posts.map(&:author_id).sort end @@ -798,7 +818,7 @@ class FinderTest < ActiveRecord::TestCase end def test_finder_with_offset_string - assert_nothing_raised(ActiveRecord::StatementInvalid) { Topic.scoped(:offset => "3").all } + assert_nothing_raised(ActiveRecord::StatementInvalid) { Topic.all.merge!(:offset => "3").to_a } end protected diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index afff020561..4c6d4666ed 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -5,10 +5,9 @@ require 'config' gem 'minitest' require 'minitest/autorun' require 'stringio' -require 'mocha' -require 'cases/test_case' require 'active_record' +require 'cases/test_case' require 'active_support/dependencies' require 'active_support/logger' @@ -20,9 +19,6 @@ require 'support/connection' # Show backtraces for deprecated behavior for quicker cleanup. ActiveSupport::Deprecation.debug = true -# Avoid deprecation warning setting dependent_restrict_raises to false. The default is true -ActiveRecord::Base.dependent_restrict_raises = false - # Connect to the database ARTest.connect diff --git a/activerecord/test/cases/inclusion_test.rb b/activerecord/test/cases/inclusion_test.rb index 8726ba5e51..8f095e4953 100644 --- a/activerecord/test/cases/inclusion_test.rb +++ b/activerecord/test/cases/inclusion_test.rb @@ -4,7 +4,7 @@ require 'models/teapot' class BasicInclusionModelTest < ActiveRecord::TestCase def test_basic_model Teapot.create!(:name => "Ronnie Kemper") - assert_equal "Ronnie Kemper", Teapot.find(1).name + assert_equal "Ronnie Kemper", Teapot.first.name end def test_initialization @@ -84,8 +84,10 @@ class InclusionUnitTest < ActiveRecord::TestCase end def test_deprecation_proxy - assert_equal ActiveRecord::Model.name, ActiveRecord::Model::DeprecationProxy.name - assert_equal ActiveRecord::Base.superclass, assert_deprecated { ActiveRecord::Model::DeprecationProxy.superclass } + proxy = ActiveRecord::Model::DeprecationProxy.new + + assert_equal ActiveRecord::Model.name, proxy.name + assert_equal ActiveRecord::Base.superclass, assert_deprecated { proxy.superclass } sup, sup2 = nil, nil ActiveSupport.on_load(:__test_active_record_model_deprecation) do @@ -93,11 +95,29 @@ class InclusionUnitTest < ActiveRecord::TestCase sup2 = send(:superclass) end assert_deprecated do - ActiveSupport.run_load_hooks(:__test_active_record_model_deprecation, ActiveRecord::Model::DeprecationProxy) + ActiveSupport.run_load_hooks(:__test_active_record_model_deprecation, proxy) end assert_equal ActiveRecord::Base.superclass, sup assert_equal ActiveRecord::Base.superclass, sup2 end + + test "including in deprecation proxy" do + model, base = ActiveRecord::Model.dup, ActiveRecord::Base.dup + proxy = ActiveRecord::Model::DeprecationProxy.new(model, base) + + mod = Module.new + proxy.include mod + assert model < mod + end + + test "extending in deprecation proxy" do + model, base = ActiveRecord::Model.dup, ActiveRecord::Base.dup + proxy = ActiveRecord::Model::DeprecationProxy.new(model, base) + + mod = Module.new + assert_deprecated { proxy.extend mod } + assert base.singleton_class < mod + end end class InclusionFixturesTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb index 06de51f5cd..e80259a7f1 100644 --- a/activerecord/test/cases/inheritance_test.rb +++ b/activerecord/test/cases/inheritance_test.rb @@ -1,7 +1,10 @@ require "cases/helper" require 'models/company' +require 'models/person' +require 'models/post' require 'models/project' require 'models/subscriber' +require 'models/teapot' class InheritanceTest < ActiveRecord::TestCase fixtures :companies, :projects, :subscribers, :accounts @@ -24,7 +27,7 @@ class InheritanceTest < ActiveRecord::TestCase end }) company.save! - company = Company.all.find { |x| x.id == company.id } + company = Company.all.to_a.find { |x| x.id == company.id } assert_equal ' ', company.type end @@ -70,6 +73,33 @@ class InheritanceTest < ActiveRecord::TestCase assert !Class.new(Company).descends_from_active_record?, 'Company subclass should not descend from ActiveRecord::Base' end + def test_inheritance_base_class + assert_equal Post, Post.base_class + assert_equal Post, SpecialPost.base_class + assert_equal Post, StiPost.base_class + assert_equal SubStiPost, SubStiPost.base_class + end + + def test_active_record_model_included_base_class + assert_equal Teapot, Teapot.base_class + end + + def test_abstract_inheritance_base_class + assert_equal LoosePerson, LoosePerson.base_class + assert_equal LooseDescendant, LooseDescendant.base_class + assert_equal TightPerson, TightPerson.base_class + assert_equal TightPerson, TightDescendant.base_class + end + + def test_base_class_activerecord_error + klass = Class.new { + extend ActiveRecord::Configuration + include ActiveRecord::Inheritance + } + + assert_raise(ActiveRecord::ActiveRecordError) { klass.base_class } + end + def test_a_bad_type_column #SQLServer need to turn Identity Insert On before manually inserting into the Identity column if current_adapter?(:SybaseAdapter) @@ -98,7 +128,7 @@ class InheritanceTest < ActiveRecord::TestCase end def test_inheritance_find_all - companies = Company.scoped(:order => 'id').all + companies = Company.all.merge!(:order => 'id').to_a assert_kind_of Firm, companies[0], "37signals should be a firm" assert_kind_of Client, companies[1], "Summit should be a client" end @@ -149,9 +179,9 @@ class InheritanceTest < ActiveRecord::TestCase def test_update_all_within_inheritance Client.update_all "name = 'I am a client'" - assert_equal "I am a client", Client.all.first.name + assert_equal "I am a client", Client.first.name # Order by added as otherwise Oracle tests were failing because of different order of results - assert_equal "37signals", Firm.scoped(:order => "id").all.first.name + assert_equal "37signals", Firm.all.merge!(:order => "id").to_a.first.name end def test_alt_update_all_within_inheritance @@ -173,9 +203,9 @@ class InheritanceTest < ActiveRecord::TestCase end def test_find_first_within_inheritance - assert_kind_of Firm, Company.scoped(:where => "name = '37signals'").first - assert_kind_of Firm, Firm.scoped(:where => "name = '37signals'").first - assert_nil Client.scoped(:where => "name = '37signals'").first + assert_kind_of Firm, Company.all.merge!(:where => "name = '37signals'").first + assert_kind_of Firm, Firm.all.merge!(:where => "name = '37signals'").first + assert_nil Client.all.merge!(:where => "name = '37signals'").first end def test_alt_find_first_within_inheritance @@ -187,10 +217,10 @@ class InheritanceTest < ActiveRecord::TestCase def test_complex_inheritance very_special_client = VerySpecialClient.create("name" => "veryspecial") assert_equal very_special_client, VerySpecialClient.where("name = 'veryspecial'").first - assert_equal very_special_client, SpecialClient.scoped(:where => "name = 'veryspecial'").first - assert_equal very_special_client, Company.scoped(:where => "name = 'veryspecial'").first - assert_equal very_special_client, Client.scoped(:where => "name = 'veryspecial'").first - assert_equal 1, Client.scoped(:where => "name = 'Summit'").all.size + assert_equal very_special_client, SpecialClient.all.merge!(:where => "name = 'veryspecial'").first + assert_equal very_special_client, Company.all.merge!(:where => "name = 'veryspecial'").first + assert_equal very_special_client, Client.all.merge!(:where => "name = 'veryspecial'").first + assert_equal 1, Client.all.merge!(:where => "name = 'Summit'").to_a.size assert_equal very_special_client, Client.find(very_special_client.id) end @@ -201,14 +231,14 @@ class InheritanceTest < ActiveRecord::TestCase end def test_eager_load_belongs_to_something_inherited - account = Account.scoped(:includes => :firm).find(1) + account = Account.all.merge!(:includes => :firm).find(1) assert account.association_cache.key?(:firm), "nil proves eager load failed" end def test_eager_load_belongs_to_primary_key_quoting con = Account.connection assert_sql(/#{con.quote_table_name('companies')}.#{con.quote_column_name('id')} IN \(1\)/) do - Account.scoped(:includes => :firm).find(1) + Account.all.merge!(:includes => :firm).find(1) end end @@ -230,7 +260,7 @@ class InheritanceTest < ActiveRecord::TestCase private def switch_to_alt_inheritance_column # we don't want misleading test results, so get rid of the values in the type column - Company.scoped(:order => 'id').all.each do |c| + Company.all.merge!(:order => 'id').to_a.each do |c| c['type'] = nil c.save end diff --git a/activerecord/test/cases/json_serialization_test.rb b/activerecord/test/cases/json_serialization_test.rb index d9e350abc0..a86b165c78 100644 --- a/activerecord/test/cases/json_serialization_test.rb +++ b/activerecord/test/cases/json_serialization_test.rb @@ -83,6 +83,53 @@ class JsonSerializationTest < ActiveRecord::TestCase assert_match %r{"favorite_quote":"Constraints are liberating"}, methods_json end + def test_uses_serializable_hash_with_only_option + def @contact.serializable_hash(options=nil) + super(only: %w(name)) + end + + json = @contact.to_json + assert_match %r{"name":"Konata Izumi"}, json + assert_no_match %r{awesome}, json + assert_no_match %r{age}, json + end + + def test_uses_serializable_hash_with_except_option + def @contact.serializable_hash(options=nil) + super(except: %w(age)) + end + + json = @contact.to_json + assert_match %r{"name":"Konata Izumi"}, json + assert_match %r{"awesome":true}, json + assert_no_match %r{age}, json + end + + def test_does_not_include_inheritance_column_from_sti + @contact = ContactSti.new(@contact.attributes) + assert_equal 'ContactSti', @contact.type + + json = @contact.to_json + assert_match %r{"name":"Konata Izumi"}, json + assert_no_match %r{type}, json + assert_no_match %r{ContactSti}, json + end + + def test_serializable_hash_with_default_except_option_and_excluding_inheritance_column_from_sti + @contact = ContactSti.new(@contact.attributes) + assert_equal 'ContactSti', @contact.type + + def @contact.serializable_hash(options={}) + super({ except: %w(age) }.merge!(options)) + end + + json = @contact.to_json + assert_match %r{"name":"Konata Izumi"}, json + assert_no_match %r{age}, json + assert_no_match %r{type}, json + assert_no_match %r{ContactSti}, json + end + def test_serializable_hash_should_not_modify_options_in_argument options = { :only => :name } @contact.serializable_hash(options) diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index afb0bd6fd9..2392516395 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -3,6 +3,7 @@ require "cases/helper" require 'models/person' require 'models/job' require 'models/reader' +require 'models/ship' require 'models/legacy_thing' require 'models/reference' require 'models/string_key_object' @@ -18,8 +19,8 @@ class LockWithCustomColumnWithoutDefault < ActiveRecord::Base self.locking_column = :custom_lock_version end -class ReadonlyFirstNamePerson < Person - attr_readonly :first_name +class ReadonlyNameShip < Ship + attr_readonly :name end class OptimisticLockingTest < ActiveRecord::TestCase @@ -200,15 +201,15 @@ class OptimisticLockingTest < ActiveRecord::TestCase end def test_readonly_attributes - assert_equal Set.new([ 'first_name' ]), ReadonlyFirstNamePerson.readonly_attributes + assert_equal Set.new([ 'name' ]), ReadonlyNameShip.readonly_attributes - p = ReadonlyFirstNamePerson.create(:first_name => "unchangeable name") - p.reload - assert_equal "unchangeable name", p.first_name + s = ReadonlyNameShip.create(:name => "unchangeable name") + s.reload + assert_equal "unchangeable name", s.name - p.update_attributes(:first_name => "changed name") - p.reload - assert_equal "unchangeable name", p.first_name + s.update_attributes(:name => "changed name") + s.reload + assert_equal "unchangeable name", s.name end def test_quote_table_name diff --git a/activerecord/test/cases/log_subscriber_test.rb b/activerecord/test/cases/log_subscriber_test.rb index acd2fcdad4..70d00aecf9 100644 --- a/activerecord/test/cases/log_subscriber_test.rb +++ b/activerecord/test/cases/log_subscriber_test.rb @@ -54,7 +54,7 @@ class LogSubscriberTest < ActiveRecord::TestCase end def test_basic_query_logging - Developer.all + Developer.all.load wait assert_equal 1, @logger.logged(:debug).size assert_match(/Developer Load/, @logger.logged(:debug).last) @@ -71,8 +71,8 @@ class LogSubscriberTest < ActiveRecord::TestCase def test_cached_queries ActiveRecord::Base.cache do - Developer.all - Developer.all + Developer.all.load + Developer.all.load end wait assert_equal 2, @logger.logged(:debug).size @@ -82,7 +82,7 @@ class LogSubscriberTest < ActiveRecord::TestCase def test_basic_query_doesnt_log_when_level_is_not_debug @logger.level = INFO - Developer.all + Developer.all.load wait assert_equal 0, @logger.logged(:debug).size end @@ -90,8 +90,8 @@ class LogSubscriberTest < ActiveRecord::TestCase def test_cached_queries_doesnt_log_when_level_is_not_debug @logger.level = INFO ActiveRecord::Base.cache do - Developer.all - Developer.all + Developer.all.load + Developer.all.load end wait assert_equal 0, @logger.logged(:debug).size diff --git a/activerecord/test/cases/mass_assignment_security_test.rb b/activerecord/test/cases/mass_assignment_security_test.rb index 2f98d3c646..a36b2c2506 100644 --- a/activerecord/test/cases/mass_assignment_security_test.rb +++ b/activerecord/test/cases/mass_assignment_security_test.rb @@ -95,7 +95,11 @@ class MassAssignmentSecurityTest < ActiveRecord::TestCase end def test_mass_assigning_does_not_choke_on_nil - Firm.new.assign_attributes(nil) + assert_nil Firm.new.assign_attributes(nil) + end + + def test_mass_assigning_does_not_choke_on_empty_hash + assert_nil Firm.new.assign_attributes({}) end def test_assign_attributes_uses_default_role_when_no_role_is_provided @@ -247,10 +251,69 @@ class MassAssignmentSecurityTest < ActiveRecord::TestCase assert !Task.new.respond_to?("#{method}=") end end + + test "ActiveRecord::Model.whitelist_attributes works for models which include Model" do + begin + prev, ActiveRecord::Model.whitelist_attributes = ActiveRecord::Model.whitelist_attributes, true + + klass = Class.new { include ActiveRecord::Model } + assert_equal ActiveModel::MassAssignmentSecurity::WhiteList, klass.active_authorizers[:default].class + assert_equal [], klass.active_authorizers[:default].to_a + ensure + ActiveRecord::Model.whitelist_attributes = prev + end + end + + test "ActiveRecord::Model.whitelist_attributes works for models which inherit Base" do + begin + prev, ActiveRecord::Model.whitelist_attributes = ActiveRecord::Model.whitelist_attributes, true + + klass = Class.new(ActiveRecord::Base) + assert_equal ActiveModel::MassAssignmentSecurity::WhiteList, klass.active_authorizers[:default].class + assert_equal [], klass.active_authorizers[:default].to_a + + klass.attr_accessible 'foo' + assert_equal ['foo'], Class.new(klass).active_authorizers[:default].to_a + ensure + ActiveRecord::Model.whitelist_attributes = prev + end + end + + test "ActiveRecord::Model.mass_assignment_sanitizer works for models which include Model" do + begin + sanitizer = Object.new + prev, ActiveRecord::Model.mass_assignment_sanitizer = ActiveRecord::Model.mass_assignment_sanitizer, sanitizer + + klass = Class.new { include ActiveRecord::Model } + assert_equal sanitizer, klass._mass_assignment_sanitizer + + ActiveRecord::Model.mass_assignment_sanitizer = nil + klass = Class.new { include ActiveRecord::Model } + assert_not_nil klass._mass_assignment_sanitizer + ensure + ActiveRecord::Model.mass_assignment_sanitizer = prev + end + end + + test "ActiveRecord::Model.mass_assignment_sanitizer works for models which inherit Base" do + begin + sanitizer = Object.new + prev, ActiveRecord::Model.mass_assignment_sanitizer = ActiveRecord::Model.mass_assignment_sanitizer, sanitizer + + klass = Class.new(ActiveRecord::Base) + assert_equal sanitizer, klass._mass_assignment_sanitizer + + sanitizer2 = Object.new + klass.mass_assignment_sanitizer = sanitizer2 + assert_equal sanitizer2, Class.new(klass)._mass_assignment_sanitizer + ensure + ActiveRecord::Model.mass_assignment_sanitizer = prev + end + end end -# This class should be deleted when we removed active_record_deprecated_finders as a +# This class should be deleted when we remove activerecord-deprecated_finders as a # dependency. class MassAssignmentSecurityDeprecatedFindersTest < ActiveRecord::TestCase include MassAssignmentTestHelpers @@ -878,4 +941,26 @@ class MassAssignmentSecurityNestedAttributesTest < ActiveRecord::TestCase assert_all_attributes(person.best_friends.first) end + def test_mass_assignment_options_are_reset_after_exception + person = NestedPerson.create!({ :first_name => 'David', :gender => 'm' }, :as => :admin) + person.create_best_friend!({ :first_name => 'Jeremy', :gender => 'm' }, :as => :admin) + + attributes = { :best_friend_attributes => { :comments => 'rides a sweet bike' } } + assert_raises(RuntimeError) { person.assign_attributes(attributes, :as => :admin) } + assert_equal 'm', person.best_friend.gender + + person.best_friend_attributes = { :gender => 'f' } + assert_equal 'm', person.best_friend.gender + end + + def test_mass_assignment_options_are_nested_correctly + person = NestedPerson.create!({ :first_name => 'David', :gender => 'm' }, :as => :admin) + person.create_best_friend!({ :first_name => 'Jeremy', :gender => 'm' }, :as => :admin) + + attributes = { :best_friend_first_name => 'Josh', :best_friend_attributes => { :gender => 'f' } } + person.assign_attributes(attributes, :as => :admin) + assert_equal 'Josh', person.best_friend.first_name + assert_equal 'f', person.best_friend.gender + end + end diff --git a/activerecord/test/cases/migration/change_schema_test.rb b/activerecord/test/cases/migration/change_schema_test.rb index ab61a4dcef..ec4c554abb 100644 --- a/activerecord/test/cases/migration/change_schema_test.rb +++ b/activerecord/test/cases/migration/change_schema_test.rb @@ -141,8 +141,8 @@ module ActiveRecord created_at_column = created_columns.detect {|c| c.name == 'created_at' } updated_at_column = created_columns.detect {|c| c.name == 'updated_at' } - assert !created_at_column.null - assert !updated_at_column.null + assert created_at_column.null + assert updated_at_column.null end def test_create_table_with_timestamps_should_create_datetime_columns_with_options @@ -291,14 +291,20 @@ module ActiveRecord def test_column_exists_with_definition connection.create_table :testings do |t| - t.column :foo, :string, :limit => 100 - t.column :bar, :decimal, :precision => 8, :scale => 2 + t.column :foo, :string, limit: 100 + t.column :bar, :decimal, precision: 8, scale: 2 + t.column :taggable_id, :integer, null: false + t.column :taggable_type, :string, default: 'Photo' end - assert connection.column_exists?(:testings, :foo, :string, :limit => 100) - refute connection.column_exists?(:testings, :foo, :string, :limit => 50) - assert connection.column_exists?(:testings, :bar, :decimal, :precision => 8, :scale => 2) - refute connection.column_exists?(:testings, :bar, :decimal, :precision => 10, :scale => 2) + assert connection.column_exists?(:testings, :foo, :string, limit: 100) + refute connection.column_exists?(:testings, :foo, :string, limit: nil) + assert connection.column_exists?(:testings, :bar, :decimal, precision: 8, scale: 2) + refute connection.column_exists?(:testings, :bar, :decimal, precision: nil, scale: nil) + assert connection.column_exists?(:testings, :taggable_id, :integer, null: false) + refute connection.column_exists?(:testings, :taggable_id, :integer, null: true) + assert connection.column_exists?(:testings, :taggable_type, :string, default: 'Photo') + refute connection.column_exists?(:testings, :taggable_type, :string, default: nil) end def test_column_exists_on_table_with_no_options_parameter_supplied diff --git a/activerecord/test/cases/migration/change_table_test.rb b/activerecord/test/cases/migration/change_table_test.rb index 063209389f..4614be9650 100644 --- a/activerecord/test/cases/migration/change_table_test.rb +++ b/activerecord/test/cases/migration/change_table_test.rb @@ -30,61 +30,57 @@ module ActiveRecord def test_references_column_type_adds_id with_change_table do |t| - @connection.expect :add_column, nil, [:delete_me, 'customer_id', :integer, {}] + @connection.expect :add_reference, nil, [:delete_me, :customer, {}] t.references :customer end end def test_remove_references_column_type_removes_id with_change_table do |t| - @connection.expect :remove_column, nil, [:delete_me, 'customer_id'] + @connection.expect :remove_reference, nil, [:delete_me, :customer, {}] t.remove_references :customer end end def test_add_belongs_to_works_like_add_references with_change_table do |t| - @connection.expect :add_column, nil, [:delete_me, 'customer_id', :integer, {}] + @connection.expect :add_reference, nil, [:delete_me, :customer, {}] t.belongs_to :customer end end def test_remove_belongs_to_works_like_remove_references with_change_table do |t| - @connection.expect :remove_column, nil, [:delete_me, 'customer_id'] + @connection.expect :remove_reference, nil, [:delete_me, :customer, {}] t.remove_belongs_to :customer end end def test_references_column_type_with_polymorphic_adds_type with_change_table do |t| - @connection.expect :add_column, nil, [:delete_me, 'taggable_id', :integer, {}] - @connection.expect :add_column, nil, [:delete_me, 'taggable_type', :string, {}] - t.references :taggable, :polymorphic => true + @connection.expect :add_reference, nil, [:delete_me, :taggable, polymorphic: true] + t.references :taggable, polymorphic: true end end def test_remove_references_column_type_with_polymorphic_removes_type with_change_table do |t| - @connection.expect :remove_column, nil, [:delete_me, 'taggable_id'] - @connection.expect :remove_column, nil, [:delete_me, 'taggable_type'] - t.remove_references :taggable, :polymorphic => true + @connection.expect :remove_reference, nil, [:delete_me, :taggable, polymorphic: true] + t.remove_references :taggable, polymorphic: true end end def test_references_column_type_with_polymorphic_and_options_null_is_false_adds_table_flag with_change_table do |t| - @connection.expect :add_column, nil, [:delete_me, 'taggable_id', :integer, {:null => false}] - @connection.expect :add_column, nil, [:delete_me, 'taggable_type', :string, {:null => false}] - t.references :taggable, :polymorphic => true, :null => false + @connection.expect :add_reference, nil, [:delete_me, :taggable, polymorphic: true, null: false] + t.references :taggable, polymorphic: true, null: false end end def test_remove_references_column_type_with_polymorphic_and_options_null_is_false_removes_table_flag with_change_table do |t| - @connection.expect :remove_column, nil, [:delete_me, 'taggable_id'] - @connection.expect :remove_column, nil, [:delete_me, 'taggable_type'] - t.remove_references :taggable, :polymorphic => true, :null => false + @connection.expect :remove_reference, nil, [:delete_me, :taggable, polymorphic: true, null: false] + t.remove_references :taggable, polymorphic: true, null: false end end diff --git a/activerecord/test/cases/migration/column_attributes_test.rb b/activerecord/test/cases/migration/column_attributes_test.rb index 18f8d82bfe..b88db384a0 100644 --- a/activerecord/test/cases/migration/column_attributes_test.rb +++ b/activerecord/test/cases/migration/column_attributes_test.rb @@ -7,6 +7,14 @@ module ActiveRecord self.use_transactional_fixtures = false + def test_add_column_newline_default + string = "foo\nbar" + add_column 'test_models', 'command', :string, :default => string + TestModel.reset_column_information + + assert_equal string, TestModel.new.command + end + def test_add_remove_single_field_using_string_arguments refute TestModel.column_methods_hash.key?(:last_name) @@ -54,13 +62,13 @@ module ActiveRecord # Do a manual insertion if current_adapter?(:OracleAdapter) - connection.execute "insert into test_models (id, wealth, created_at, updated_at) values (people_seq.nextval, 12345678901234567890.0123456789, sysdate, sysdate)" + connection.execute "insert into test_models (id, wealth) values (people_seq.nextval, 12345678901234567890.0123456789)" elsif current_adapter?(:OpenBaseAdapter) || (current_adapter?(:MysqlAdapter) && Mysql.client_version < 50003) #before mysql 5.0.3 decimals stored as strings - connection.execute "insert into test_models (wealth, created_at, updated_at) values ('12345678901234567890.0123456789', 0, 0)" + connection.execute "insert into test_models (wealth) values ('12345678901234567890.0123456789')" elsif current_adapter?(:PostgreSQLAdapter) - connection.execute "insert into test_models (wealth, created_at, updated_at) values (12345678901234567890.0123456789, now(), now())" + connection.execute "insert into test_models (wealth) values (12345678901234567890.0123456789)" else - connection.execute "insert into test_models (wealth, created_at, updated_at) values (12345678901234567890.0123456789, 0, 0)" + connection.execute "insert into test_models (wealth) values (12345678901234567890.0123456789)" end # SELECT @@ -174,7 +182,7 @@ module ActiveRecord assert_not_equal "Z", bob.moment_of_truth.zone # US/Eastern is -5 hours from GMT assert_equal Rational(-5, 24), bob.moment_of_truth.offset - assert_match(/\A-05:?00\Z/, bob.moment_of_truth.zone) #ruby 1.8.6 uses HH:MM, prior versions use HHMM + assert_match(/\A-05:00\Z/, bob.moment_of_truth.zone) assert_equal DateTime::ITALY, bob.moment_of_truth.start end end diff --git a/activerecord/test/cases/migration/command_recorder_test.rb b/activerecord/test/cases/migration/command_recorder_test.rb index 7d026961be..f2213ee6aa 100644 --- a/activerecord/test/cases/migration/command_recorder_test.rb +++ b/activerecord/test/cases/migration/command_recorder_test.rb @@ -110,9 +110,9 @@ module ActiveRecord 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 + @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 @@ -138,6 +138,30 @@ module ActiveRecord add = @recorder.inverse.first assert_equal [:add_timestamps, [:table]], add end + + def test_invert_add_reference + @recorder.record :add_reference, [:table, :taggable, { polymorphic: true }] + remove = @recorder.inverse.first + assert_equal [:remove_reference, [:table, :taggable, { polymorphic: true }]], remove + end + + def test_invert_add_belongs_to_alias + @recorder.record :add_belongs_to, [:table, :user] + remove = @recorder.inverse.first + assert_equal [:remove_reference, [:table, :user]], remove + end + + def test_invert_remove_reference + @recorder.record :remove_reference, [:table, :taggable, { polymorphic: true }] + add = @recorder.inverse.first + assert_equal [:add_reference, [:table, :taggable, { polymorphic: true }]], add + end + + def test_invert_remove_belongs_to_alias + @recorder.record :remove_belongs_to, [:table, :user] + add = @recorder.inverse.first + assert_equal [:add_reference, [:table, :user]], add + end end end end diff --git a/activerecord/test/cases/migration/create_join_table_test.rb b/activerecord/test/cases/migration/create_join_table_test.rb index 0428d9ba76..cd1b0e8b47 100644 --- a/activerecord/test/cases/migration/create_join_table_test.rb +++ b/activerecord/test/cases/migration/create_join_table_test.rb @@ -10,60 +10,67 @@ module ActiveRecord @connection = ActiveRecord::Base.connection end + def teardown + super + %w(artists_musics musics_videos catalog).each do |table_name| + connection.drop_table table_name if connection.tables.include?(table_name) + end + end + def test_create_join_table connection.create_join_table :artists, :musics assert_equal %w(artist_id music_id), connection.columns(:artists_musics).map(&:name).sort - ensure - connection.drop_table :artists_musics end def test_create_join_table_set_not_null_by_default connection.create_join_table :artists, :musics assert_equal [false, false], connection.columns(:artists_musics).map(&:null) - ensure - connection.drop_table :artists_musics end def test_create_join_table_with_strings connection.create_join_table 'artists', 'musics' assert_equal %w(artist_id music_id), connection.columns(:artists_musics).map(&:name).sort - ensure - connection.drop_table :artists_musics end def test_create_join_table_with_the_proper_order connection.create_join_table :videos, :musics assert_equal %w(music_id video_id), connection.columns(:musics_videos).map(&:name).sort - ensure - connection.drop_table :musics_videos end def test_create_join_table_with_the_table_name - connection.create_join_table :artists, :musics, :table_name => :catalog + connection.create_join_table :artists, :musics, table_name: :catalog assert_equal %w(artist_id music_id), connection.columns(:catalog).map(&:name).sort - ensure - connection.drop_table :catalog end def test_create_join_table_with_the_table_name_as_string - connection.create_join_table :artists, :musics, :table_name => 'catalog' + connection.create_join_table :artists, :musics, table_name: 'catalog' assert_equal %w(artist_id music_id), connection.columns(:catalog).map(&:name).sort - ensure - connection.drop_table :catalog end def test_create_join_table_with_column_options - connection.create_join_table :artists, :musics, :column_options => {:null => true} + connection.create_join_table :artists, :musics, column_options: {null: true} assert_equal [true, true], connection.columns(:artists_musics).map(&:null) - ensure - connection.drop_table :artists_musics + end + + def test_create_join_table_without_indexes + connection.create_join_table :artists, :musics + + assert connection.indexes(:artists_musics).blank? + end + + def test_create_join_table_with_index + connection.create_join_table :artists, :musics do |t| + t.index [:artist_id, :music_id] + end + + assert_equal [%w(artist_id music_id)], connection.indexes(:artists_musics).map(&:columns) end end end diff --git a/activerecord/test/cases/migration/helper.rb b/activerecord/test/cases/migration/helper.rb index fe53510ba2..768ebc5861 100644 --- a/activerecord/test/cases/migration/helper.rb +++ b/activerecord/test/cases/migration/helper.rb @@ -14,8 +14,10 @@ module ActiveRecord module TestHelper attr_reader :connection, :table_name + CONNECTION_METHODS = %w[add_column remove_column rename_column add_index change_column rename_table column_exists? index_exists? add_reference add_belongs_to remove_reference remove_references remove_belongs_to] + class TestModel < ActiveRecord::Base - self.table_name = 'test_models' + self.table_name = :test_models end def setup @@ -36,29 +38,8 @@ module ActiveRecord end private - def add_column(*args) - connection.add_column(*args) - end - - def remove_column(*args) - connection.remove_column(*args) - end - - def rename_column(*args) - connection.rename_column(*args) - end - def add_index(*args) - connection.add_index(*args) - end - - def change_column(*args) - connection.change_column(*args) - end - - def rename_table(*args) - connection.rename_table(*args) - end + delegate(*CONNECTION_METHODS, to: :connection) end end end diff --git a/activerecord/test/cases/migration/references_index_test.rb b/activerecord/test/cases/migration/references_index_test.rb index 8ab1c59724..264a99f9ce 100644 --- a/activerecord/test/cases/migration/references_index_test.rb +++ b/activerecord/test/cases/migration/references_index_test.rb @@ -51,6 +51,8 @@ module ActiveRecord end def test_creates_polymorphic_index + return skip "Oracle Adapter does not support foreign keys if :polymorphic => true is used" if current_adapter? :OracleAdapter + connection.create_table table_name do |t| t.references :foo, :polymorphic => true, :index => true end @@ -86,6 +88,7 @@ module ActiveRecord end def test_creates_polymorphic_index_for_existing_table + return skip "Oracle Adapter does not support foreign keys if :polymorphic => true is used" if current_adapter? :OracleAdapter connection.create_table table_name connection.change_table table_name do |t| t.references :foo, :polymorphic => true, :index => true diff --git a/activerecord/test/cases/migration/references_statements_test.rb b/activerecord/test/cases/migration/references_statements_test.rb new file mode 100644 index 0000000000..144302bd4a --- /dev/null +++ b/activerecord/test/cases/migration/references_statements_test.rb @@ -0,0 +1,111 @@ +require "cases/migration/helper" + +module ActiveRecord + class Migration + class ReferencesStatementsTest < ActiveRecord::TestCase + include ActiveRecord::Migration::TestHelper + + self.use_transactional_fixtures = false + + def setup + super + @table_name = :test_models + + add_column table_name, :supplier_id, :integer + add_index table_name, :supplier_id + end + + def test_creates_reference_id_column + add_reference table_name, :user + assert column_exists?(table_name, :user_id, :integer) + end + + def test_does_not_create_reference_type_column + add_reference table_name, :taggable + refute column_exists?(table_name, :taggable_type, :string) + end + + def test_creates_reference_type_column + add_reference table_name, :taggable, polymorphic: true + assert column_exists?(table_name, :taggable_type, :string) + end + + def test_creates_reference_id_index + add_reference table_name, :user, index: true + assert index_exists?(table_name, :user_id) + end + + def test_does_not_create_reference_id_index + add_reference table_name, :user + refute index_exists?(table_name, :user_id) + end + + def test_creates_polymorphic_index + add_reference table_name, :taggable, polymorphic: true, index: true + assert index_exists?(table_name, [:taggable_id, :taggable_type]) + end + + def test_creates_reference_type_column_with_default + add_reference table_name, :taggable, polymorphic: { default: 'Photo' }, index: true + assert column_exists?(table_name, :taggable_type, :string, default: 'Photo') + end + + def test_creates_named_index + add_reference table_name, :tag, index: { name: 'index_taggings_on_tag_id' } + assert index_exists?(table_name, :tag_id, name: 'index_taggings_on_tag_id') + end + + def test_deletes_reference_id_column + remove_reference table_name, :supplier + refute column_exists?(table_name, :supplier_id, :integer) + end + + def test_deletes_reference_id_index + remove_reference table_name, :supplier + refute index_exists?(table_name, :supplier_id) + end + + def test_does_not_delete_reference_type_column + with_polymorphic_column do + remove_reference table_name, :supplier + + refute column_exists?(table_name, :supplier_id, :integer) + assert column_exists?(table_name, :supplier_type, :string) + end + end + + def test_deletes_reference_type_column + with_polymorphic_column do + remove_reference table_name, :supplier, polymorphic: true + refute column_exists?(table_name, :supplier_type, :string) + end + end + + def test_deletes_polymorphic_index + with_polymorphic_column do + remove_reference table_name, :supplier, polymorphic: true + refute index_exists?(table_name, [:supplier_id, :supplier_type]) + end + end + + def test_add_belongs_to_alias + add_belongs_to table_name, :user + assert column_exists?(table_name, :user_id, :integer) + end + + def test_remove_belongs_to_alias + remove_belongs_to table_name, :supplier + refute column_exists?(table_name, :supplier_id, :integer) + end + + private + + def with_polymorphic_column + add_column table_name, :supplier_type, :string + add_index table_name, [:supplier_id, :supplier_type] + + yield + end + end + end +end diff --git a/activerecord/test/cases/migration/rename_table_test.rb b/activerecord/test/cases/migration/rename_table_test.rb index d5ff2c607f..21901bec3c 100644 --- a/activerecord/test/cases/migration/rename_table_test.rb +++ b/activerecord/test/cases/migration/rename_table_test.rb @@ -14,6 +14,11 @@ module ActiveRecord remove_column 'test_models', :updated_at end + def teardown + rename_table :octopi, :test_models if connection.table_exists? :octopi + super + end + def test_rename_table_for_sqlite_should_work_with_reserved_words renamed = false @@ -26,8 +31,7 @@ module ActiveRecord renamed = true # Using explicit id in insert for compatibility across all databases - con = connection - con.execute "INSERT INTO 'references' (url, created_at, updated_at) VALUES ('http://rubyonrails.com', 0, 0)" + connection.execute "INSERT INTO 'references' (url, created_at, updated_at) VALUES ('http://rubyonrails.com', 0, 0)" assert_equal 'http://rubyonrails.com', connection.select_value("SELECT url FROM 'references' WHERE id=1") ensure return unless renamed @@ -39,16 +43,13 @@ module ActiveRecord rename_table :test_models, :octopi # Using explicit id in insert for compatibility across all databases - con = connection - con.enable_identity_insert("octopi", true) if current_adapter?(:SybaseAdapter) + connection.enable_identity_insert("octopi", true) if current_adapter?(:SybaseAdapter) - con.execute "INSERT INTO octopi (#{con.quote_column_name('id')}, #{con.quote_column_name('url')}) VALUES (1, 'http://www.foreverflying.com/octopus-black7.jpg')" + connection.execute "INSERT INTO octopi (#{connection.quote_column_name('id')}, #{connection.quote_column_name('url')}) VALUES (1, 'http://www.foreverflying.com/octopus-black7.jpg')" - con.enable_identity_insert("octopi", false) if current_adapter?(:SybaseAdapter) + connection.enable_identity_insert("octopi", false) if current_adapter?(:SybaseAdapter) assert_equal 'http://www.foreverflying.com/octopus-black7.jpg', connection.select_value("SELECT url FROM octopi WHERE id=1") - - rename_table :octopi, :test_models end def test_rename_table_with_an_index @@ -57,15 +58,22 @@ module ActiveRecord rename_table :test_models, :octopi # Using explicit id in insert for compatibility across all databases - con = ActiveRecord::Base.connection - con.enable_identity_insert("octopi", true) if current_adapter?(:SybaseAdapter) - con.execute "INSERT INTO octopi (#{con.quote_column_name('id')}, #{con.quote_column_name('url')}) VALUES (1, 'http://www.foreverflying.com/octopus-black7.jpg')" - con.enable_identity_insert("octopi", false) if current_adapter?(:SybaseAdapter) + connection.enable_identity_insert("octopi", true) if current_adapter?(:SybaseAdapter) + connection.execute "INSERT INTO octopi (#{connection.quote_column_name('id')}, #{connection.quote_column_name('url')}) VALUES (1, 'http://www.foreverflying.com/octopus-black7.jpg')" + connection.enable_identity_insert("octopi", false) if current_adapter?(:SybaseAdapter) assert_equal 'http://www.foreverflying.com/octopus-black7.jpg', connection.select_value("SELECT url FROM octopi WHERE id=1") assert connection.indexes(:octopi).first.columns.include?("url") + end + + def test_rename_table_for_postgresql_should_also_rename_default_sequence + skip 'not supported' unless current_adapter?(:PostgreSQLAdapter) + + rename_table :test_models, :octopi + + pk, seq = connection.pk_and_sequence_for('octopi') - rename_table :octopi, :test_models + assert_equal "octopi_#{pk}_seq", seq end end end diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index 5d1bad0d54..3c0d2b18d9 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -56,6 +56,21 @@ class MigrationTest < ActiveRecord::TestCase Person.reset_column_information end + def test_migrator_versions + migrations_path = MIGRATIONS_ROOT + "/valid" + ActiveRecord::Migrator.migrations_paths = migrations_path + + ActiveRecord::Migrator.up(migrations_path) + assert_equal 3, ActiveRecord::Migrator.current_version + assert_equal 3, ActiveRecord::Migrator.last_version + assert_equal false, ActiveRecord::Migrator.needs_migration? + + ActiveRecord::Migrator.down(MIGRATIONS_ROOT + "/valid") + assert_equal 0, ActiveRecord::Migrator.current_version + assert_equal 3, ActiveRecord::Migrator.last_version + assert_equal true, ActiveRecord::Migrator.needs_migration? + end + def test_create_table_with_force_true_does_not_drop_nonexisting_table if Person.connection.table_exists?(:testings2) Person.connection.drop_table :testings2 @@ -523,7 +538,7 @@ if ActiveRecord::Base.connection.supports_bulk_alter? # One query for columns (delete_me table) # One query for primary key (delete_me table) # One query to do the bulk change - assert_queries(3) do + assert_queries(3, :ignore_none => true) do with_bulk_change_table do |t| t.change :name, :string, :default => 'NONAME' t.change :birthdate, :datetime diff --git a/activerecord/test/cases/modules_test.rb b/activerecord/test/cases/modules_test.rb index a03c4f552e..08b3408665 100644 --- a/activerecord/test/cases/modules_test.rb +++ b/activerecord/test/cases/modules_test.rb @@ -39,7 +39,7 @@ class ModulesTest < ActiveRecord::TestCase end def test_associations_spanning_cross_modules - account = MyApplication::Billing::Account.scoped(:order => 'id').first + account = MyApplication::Billing::Account.all.merge!(:order => 'id').first assert_kind_of MyApplication::Business::Firm, account.firm assert_kind_of MyApplication::Billing::Firm, account.qualified_billing_firm assert_kind_of MyApplication::Billing::Firm, account.unqualified_billing_firm @@ -48,7 +48,7 @@ class ModulesTest < ActiveRecord::TestCase end def test_find_account_and_include_company - account = MyApplication::Billing::Account.scoped(:includes => :firm).find(1) + account = MyApplication::Billing::Account.all.merge!(:includes => :firm).find(1) assert_kind_of MyApplication::Business::Firm, account.firm end @@ -72,8 +72,8 @@ class ModulesTest < ActiveRecord::TestCase clients = [] assert_nothing_raised NameError, "Should be able to resolve all class constants via reflection" do - clients << MyApplication::Business::Client.references(:accounts).scoped(:includes => {:firm => :account}, :where => 'accounts.id IS NOT NULL').find(3) - clients << MyApplication::Business::Client.scoped(:includes => {:firm => :account}).find(3) + clients << MyApplication::Business::Client.references(:accounts).merge!(:includes => {:firm => :account}, :where => 'accounts.id IS NOT NULL').find(3) + clients << MyApplication::Business::Client.includes(:firm => :account).find(3) end clients.each do |client| diff --git a/activerecord/test/cases/multiple_db_test.rb b/activerecord/test/cases/multiple_db_test.rb index 06d6596725..42461e8ecb 100644 --- a/activerecord/test/cases/multiple_db_test.rb +++ b/activerecord/test/cases/multiple_db_test.rb @@ -1,9 +1,7 @@ require "cases/helper" require 'models/entrant' require 'models/bird' - -# So we can test whether Course.connection survives a reload. -require_dependency 'models/course' +require 'models/course' class MultipleDbTest < ActiveRecord::TestCase self.use_transactional_fixtures = false diff --git a/activerecord/test/cases/named_scope_test.rb b/activerecord/test/cases/named_scope_test.rb index bf825c002a..bd121126e7 100644 --- a/activerecord/test/cases/named_scope_test.rb +++ b/activerecord/test/cases/named_scope_test.rb @@ -12,14 +12,13 @@ class NamedScopeTest < ActiveRecord::TestCase def test_implements_enumerable assert !Topic.all.empty? - assert_equal Topic.all, Topic.base - assert_equal Topic.all, Topic.base.to_a - assert_equal Topic.first, Topic.base.first - assert_equal Topic.all, Topic.base.map { |i| i } + assert_equal Topic.all.to_a, Topic.base + assert_equal Topic.all.to_a, Topic.base.to_a + assert_equal Topic.first, Topic.base.first + assert_equal Topic.all.to_a, Topic.base.map { |i| i } end def test_found_items_are_cached - Topic.columns all_posts = Topic.base assert_queries(1) do @@ -30,7 +29,7 @@ class NamedScopeTest < ActiveRecord::TestCase def test_reload_expires_cache_of_found_items all_posts = Topic.base - all_posts.all + all_posts.to_a new_post = Topic.create! assert !all_posts.include?(new_post) @@ -40,12 +39,21 @@ class NamedScopeTest < ActiveRecord::TestCase def test_delegates_finds_and_calculations_to_the_base_class assert !Topic.all.empty? - assert_equal Topic.all, Topic.base.all - assert_equal Topic.first, Topic.base.first - assert_equal Topic.count, Topic.base.count + assert_equal Topic.all.to_a, Topic.base.to_a + assert_equal Topic.first, Topic.base.first + assert_equal Topic.count, Topic.base.count assert_equal Topic.average(:replies_count), Topic.base.average(:replies_count) end + def test_method_missing_priority_when_delegating + klazz = Class.new(ActiveRecord::Base) do + self.table_name = "topics" + scope :since, Proc.new { where('written_on >= ?', Time.now - 1.day) } + scope :to, Proc.new { where('written_on <= ?', Time.now) } + end + assert_equal klazz.to.since.to_a, klazz.since.to.to_a + end + def test_scope_should_respond_to_own_methods_and_methods_of_the_proxy assert Topic.approved.respond_to?(:limit) assert Topic.approved.respond_to?(:count) @@ -58,9 +66,9 @@ class NamedScopeTest < ActiveRecord::TestCase end def test_scopes_with_options_limit_finds_to_those_matching_the_criteria_specified - assert !Topic.scoped(:where => {:approved => true}).all.empty? + assert !Topic.all.merge!(:where => {:approved => true}).to_a.empty? - assert_equal Topic.scoped(:where => {:approved => true}).all, Topic.approved + assert_equal Topic.all.merge!(:where => {:approved => true}).to_a, Topic.approved assert_equal Topic.where(:approved => true).count, Topic.approved.count end @@ -71,8 +79,8 @@ class NamedScopeTest < ActiveRecord::TestCase end def test_scopes_are_composable - assert_equal((approved = Topic.scoped(:where => {:approved => true}).all), Topic.approved) - assert_equal((replied = Topic.scoped(:where => 'replies_count > 0').all), Topic.replied) + assert_equal((approved = Topic.all.merge!(:where => {:approved => true}).to_a), Topic.approved) + assert_equal((replied = Topic.all.merge!(:where => 'replies_count > 0').to_a), Topic.replied) assert !(approved == replied) assert !(approved & replied).empty? @@ -132,14 +140,14 @@ class NamedScopeTest < ActiveRecord::TestCase def test_active_records_have_scope_named__all__ assert !Topic.all.empty? - assert_equal Topic.all, Topic.base + assert_equal Topic.all.to_a, Topic.base end def test_active_records_have_scope_named__scoped__ scope = Topic.where("content LIKE '%Have%'") assert !scope.empty? - assert_equal scope, Topic.scoped(where: "content LIKE '%Have%'") + assert_equal scope, Topic.all.merge!(where: "content LIKE '%Have%'") end def test_first_and_last_should_allow_integers_for_limit @@ -317,14 +325,14 @@ class NamedScopeTest < ActiveRecord::TestCase def test_chaining_should_use_latest_conditions_when_searching # Normal hash conditions - assert_equal Topic.where(:approved => true).to_a, Topic.rejected.approved.all - assert_equal Topic.where(:approved => false).to_a, Topic.approved.rejected.all + assert_equal Topic.where(:approved => true).to_a, Topic.rejected.approved.to_a + assert_equal Topic.where(:approved => false).to_a, Topic.approved.rejected.to_a # Nested hash conditions with same keys - assert_equal [posts(:sti_comments)], Post.with_special_comments.with_very_special_comments.all + assert_equal [posts(:sti_comments)], Post.with_special_comments.with_very_special_comments.to_a # Nested hash conditions with different keys - assert_equal [posts(:sti_comments)], Post.with_special_comments.with_post(4).all.uniq + assert_equal [posts(:sti_comments)], Post.with_special_comments.with_post(4).to_a.uniq end def test_scopes_batch_finders @@ -343,13 +351,13 @@ class NamedScopeTest < ActiveRecord::TestCase def test_table_names_for_chaining_scopes_with_and_without_table_name_included assert_nothing_raised do - Comment.for_first_post.for_first_author.all + Comment.for_first_post.for_first_author.to_a end end def test_scopes_on_relations # Topic.replied - approved_topics = Topic.scoped.approved.order('id DESC') + approved_topics = Topic.all.approved.order('id DESC') assert_equal topics(:fourth), approved_topics.first replied_approved_topics = approved_topics.replied @@ -364,7 +372,7 @@ class NamedScopeTest < ActiveRecord::TestCase def test_nested_scopes_queries_size assert_queries(1) do - Topic.approved.by_lifo.replied.written_before(Time.now).all + Topic.approved.by_lifo.replied.written_before(Time.now).to_a end end @@ -375,8 +383,8 @@ class NamedScopeTest < ActiveRecord::TestCase post = posts(:welcome) Post.cache do - assert_queries(1) { post.comments.containing_the_letter_e.all } - assert_no_queries { post.comments.containing_the_letter_e.all } + assert_queries(1) { post.comments.containing_the_letter_e.to_a } + assert_no_queries { post.comments.containing_the_letter_e.to_a } end end @@ -384,14 +392,14 @@ class NamedScopeTest < ActiveRecord::TestCase post = posts(:welcome) Post.cache do - one = assert_queries(1) { post.comments.limit_by(1).all } + one = assert_queries(1) { post.comments.limit_by(1).to_a } assert_equal 1, one.size - two = assert_queries(1) { post.comments.limit_by(2).all } + two = assert_queries(1) { post.comments.limit_by(2).to_a } assert_equal 2, two.size - assert_no_queries { post.comments.limit_by(1).all } - assert_no_queries { post.comments.limit_by(2).all } + assert_no_queries { post.comments.limit_by(1).to_a } + assert_no_queries { post.comments.limit_by(2).to_a } end end diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb index 0559bbbe9a..3a234f0cc1 100644 --- a/activerecord/test/cases/nested_attributes_test.rb +++ b/activerecord/test/cases/nested_attributes_test.rb @@ -196,7 +196,7 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase end def test_should_raise_argument_error_if_trying_to_build_polymorphic_belongs_to - assert_raise_with_message ArgumentError, "Cannot build association looter. Are you trying to build a polymorphic one-to-one association?" do + assert_raise_with_message ArgumentError, "Cannot build association `looter'. Are you trying to build a polymorphic one-to-one association?" do Treasure.new(:name => 'pearl', :looter_attributes => {:catchphrase => "Arrr"}) end end @@ -781,6 +781,16 @@ module NestedAttributesOnACollectionAssociationTests assert_nothing_raised(NoMethodError) { @pirate.save! } end + def test_numeric_colum_changes_from_zero_to_no_empty_string + Man.accepts_nested_attributes_for(:interests) + Interest.validates_numericality_of(:zine_id) + man = Man.create(:name => 'John') + interest = man.interests.create(:topic=>'bar',:zine_id => 0) + assert interest.save + + assert !man.update_attributes({:interests_attributes => { :id => interest.id, :zine_id => 'foo' }}) + end + private def association_setter diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index 0933a4ff3d..72b8219782 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -55,7 +55,7 @@ class PersistencesTest < ActiveRecord::TestCase author = authors(:david) assert_nothing_raised do assert_equal 1, author.posts_sorted_by_id_limited.size - assert_equal 2, author.posts_sorted_by_id_limited.scoped(:limit => 2).all.size + assert_equal 2, author.posts_sorted_by_id_limited.limit(2).to_a.size assert_equal 1, author.posts_sorted_by_id_limited.update_all([ "body = ?", "bulk update!" ]) assert_equal "bulk update!", posts(:welcome).body assert_not_equal "bulk update!", posts(:thinking).body @@ -120,7 +120,7 @@ class PersistencesTest < ActiveRecord::TestCase def test_destroy_all conditions = "author_name = 'Mary'" - topics_by_mary = Topic.scoped(:where => conditions, :order => 'id').to_a + topics_by_mary = Topic.all.merge!(:where => conditions, :order => 'id').to_a assert ! topics_by_mary.empty? assert_difference('Topic.count', -topics_by_mary.size) do @@ -131,7 +131,7 @@ class PersistencesTest < ActiveRecord::TestCase end def test_destroy_many - clients = Client.scoped(:order => 'id').find([2, 3]) + clients = Client.all.merge!(:order => 'id').find([2, 3]) assert_difference('Client.count', -2) do destroyed = Client.destroy([2, 3]).sort_by(&:id) @@ -305,6 +305,13 @@ class PersistencesTest < ActiveRecord::TestCase assert_raise(ActiveRecord::RecordNotFound) { Topic.find(topic.id) } end + def test_destroy! + topic = Topic.find(1) + assert_equal topic, topic.destroy!, 'topic.destroy! did not return self' + assert topic.frozen?, 'topic not frozen after destroy!' + assert_raise(ActiveRecord::RecordNotFound) { Topic.find(topic.id) } + end + def test_record_not_found_exception assert_raise(ActiveRecord::RecordNotFound) { Topic.find(99999) } end @@ -364,71 +371,10 @@ class PersistencesTest < ActiveRecord::TestCase assert_raise(ActiveSupport::FrozenObjectError) { client.name = "something else" } end - def test_update_attribute - assert !Topic.find(1).approved? - Topic.find(1).update_attribute("approved", true) - assert Topic.find(1).approved? - - Topic.find(1).update_attribute(:approved, false) - assert !Topic.find(1).approved? - end - def test_update_attribute_does_not_choke_on_nil assert Topic.find(1).update_attributes(nil) end - def test_update_attribute_for_readonly_attribute - minivan = Minivan.find('m1') - assert_raises(ActiveRecord::ActiveRecordError) { minivan.update_attribute(:color, 'black') } - end - - # This test is correct, but it is hard to fix it since - # update_attribute trigger simply call save! that triggers - # all callbacks. - # def test_update_attribute_with_one_changed_and_one_updated - # t = Topic.order('id').limit(1).first - # title, author_name = t.title, t.author_name - # t.author_name = 'John' - # t.update_attribute(:title, 'super_title') - # assert_equal 'John', t.author_name - # assert_equal 'super_title', t.title - # assert t.changed?, "topic should have changed" - # assert t.author_name_changed?, "author_name should have changed" - # assert !t.title_changed?, "title should not have changed" - # assert_nil t.title_change, 'title change should be nil' - # assert_equal ['author_name'], t.changed - # - # t.reload - # assert_equal 'David', t.author_name - # assert_equal 'super_title', t.title - # end - - def test_update_attribute_with_one_updated - t = Topic.first - t.update_attribute(:title, 'super_title') - assert_equal 'super_title', t.title - assert !t.changed?, "topic should not have changed" - assert !t.title_changed?, "title should not have changed" - assert_nil t.title_change, 'title change should be nil' - - t.reload - assert_equal 'super_title', t.title - end - - def test_update_attribute_for_updated_at_on - developer = Developer.find(1) - prev_month = Time.now.prev_month - - developer.update_attribute(:updated_at, prev_month) - assert_equal prev_month, developer.updated_at - - developer.update_attribute(:salary, 80001) - assert_not_equal prev_month, developer.updated_at - - developer.reload - assert_not_equal prev_month, developer.updated_at - end - def test_update_column topic = Topic.find(1) topic.update_column("approved", true) @@ -460,7 +406,7 @@ class PersistencesTest < ActiveRecord::TestCase def test_update_column_should_not_leave_the_object_dirty topic = Topic.find(1) - topic.update_attribute("content", "Have a nice day") + topic.update_column("content", "Have a nice day") topic.reload topic.update_column(:content, "You too") @@ -515,6 +461,97 @@ class PersistencesTest < ActiveRecord::TestCase assert_equal 'super_title', t.title end + def test_update_columns + topic = Topic.find(1) + topic.update_columns({ "approved" => true, title: "Sebastian Topic" }) + assert topic.approved? + assert_equal "Sebastian Topic", topic.title + topic.reload + assert topic.approved? + assert_equal "Sebastian Topic", topic.title + end + + def test_update_columns_should_not_use_setter_method + dev = Developer.find(1) + dev.instance_eval { def salary=(value); write_attribute(:salary, value * 2); end } + + dev.update_columns(salary: 80000) + assert_equal 80000, dev.salary + + dev.reload + assert_equal 80000, dev.salary + end + + def test_update_columns_should_raise_exception_if_new_record + topic = Topic.new + assert_raises(ActiveRecord::ActiveRecordError) { topic.update_columns({ approved: false }) } + end + + def test_update_columns_should_not_leave_the_object_dirty + topic = Topic.find(1) + topic.update_attributes({ "content" => "Have a nice day", :author_name => "Jose" }) + + topic.reload + topic.update_columns({ content: "You too", "author_name" => "Sebastian" }) + assert_equal [], topic.changed + + topic.reload + topic.update_columns({ content: "Have a nice day", author_name: "Jose" }) + assert_equal [], topic.changed + end + + def test_update_columns_with_model_having_primary_key_other_than_id + minivan = Minivan.find('m1') + new_name = 'sebavan' + + minivan.update_columns(name: new_name) + assert_equal new_name, minivan.name + end + + def test_update_columns_with_one_readonly_attribute + minivan = Minivan.find('m1') + prev_color = minivan.color + prev_name = minivan.name + assert_raises(ActiveRecord::ActiveRecordError) { minivan.update_columns({ name: "My old minivan", color: 'black' }) } + assert_equal prev_color, minivan.color + assert_equal prev_name, minivan.name + + minivan.reload + assert_equal prev_color, minivan.color + assert_equal prev_name, minivan.name + end + + def test_update_columns_should_not_modify_updated_at + developer = Developer.find(1) + prev_month = Time.now.prev_month + + developer.update_columns(updated_at: prev_month) + assert_equal prev_month, developer.updated_at + + developer.update_columns(salary: 80000) + assert_equal prev_month, developer.updated_at + assert_equal 80000, developer.salary + + developer.reload + assert_equal prev_month.to_i, developer.updated_at.to_i + assert_equal 80000, developer.salary + end + + def test_update_columns_with_one_changed_and_one_updated + t = Topic.order('id').limit(1).first + author_name = t.author_name + t.author_name = 'John' + t.update_columns(title: 'super_title') + assert_equal 'John', t.author_name + assert_equal 'super_title', t.title + assert t.changed?, "topic should have changed" + assert t.author_name_changed?, "author_name should have changed" + + t.reload + assert_equal author_name, t.author_name + assert_equal 'super_title', t.title + end + def test_update_attributes topic = Topic.find(1) assert !topic.approved? diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb index a712e5f689..2d778e9e90 100644 --- a/activerecord/test/cases/query_cache_test.rb +++ b/activerecord/test/cases/query_cache_test.rb @@ -39,6 +39,18 @@ class QueryCacheTest < ActiveRecord::TestCase assert ActiveRecord::Base.connection.query_cache_enabled, 'cache on' end + def test_exceptional_middleware_assigns_original_connection_id_on_error + connection_id = ActiveRecord::Base.connection_id + + mw = ActiveRecord::QueryCache.new lambda { |env| + ActiveRecord::Base.connection_id = self.object_id + raise "lol borked" + } + assert_raises(RuntimeError) { mw.call({}) } + + assert_equal connection_id, ActiveRecord::Base.connection_id + end + def test_middleware_delegates called = false mw = ActiveRecord::QueryCache.new lambda { |env| @@ -151,16 +163,11 @@ class QueryCacheTest < ActiveRecord::TestCase end def test_cache_does_not_wrap_string_results_in_arrays - if current_adapter?(:SQLite3Adapter) - require 'sqlite3/version' - sqlite3_version = RUBY_PLATFORM =~ /java/ ? Jdbc::SQLite3::VERSION : SQLite3::VERSION - end - Task.cache do # Oracle adapter returns count() as Fixnum or Float if current_adapter?(:OracleAdapter) assert_kind_of Numeric, Task.connection.select_value("SELECT count(*) AS count_all FROM tasks") - elsif current_adapter?(:SQLite3Adapter) && sqlite3_version > '1.2.5' || current_adapter?(:Mysql2Adapter) || current_adapter?(:MysqlAdapter) + elsif current_adapter?(:SQLite3Adapter) || current_adapter?(:Mysql2Adapter) # Future versions of the sqlite3 adapter will return numeric assert_instance_of Fixnum, Task.connection.select_value("SELECT count(*) AS count_all FROM tasks") @@ -169,6 +176,14 @@ class QueryCacheTest < ActiveRecord::TestCase end end end + + def test_cache_is_ignored_for_locked_relations + task = Task.find 1 + + Task.cache do + assert_queries(2) { task.lock!; task.lock! } + end + end end class QueryCacheExpiryTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/quoting_test.rb b/activerecord/test/cases/quoting_test.rb index 80ee74e41e..3dd11ae89d 100644 --- a/activerecord/test/cases/quoting_test.rb +++ b/activerecord/test/cases/quoting_test.rb @@ -216,6 +216,14 @@ module ActiveRecord def test_string_with_crazy_column assert_equal "'lo\\\\l'", @quoter.quote('lo\l', FakeColumn.new(:foo)) end + + def test_quote_duration + assert_equal "1800", @quoter.quote(30.minutes) + end + + def test_quote_duration_int_column + assert_equal "7200", @quoter.quote(2.hours, FakeColumn.new(:integer)) + end end end end diff --git a/activerecord/test/cases/readonly_test.rb b/activerecord/test/cases/readonly_test.rb index df0399f548..df076c97b4 100644 --- a/activerecord/test/cases/readonly_test.rb +++ b/activerecord/test/cases/readonly_test.rb @@ -49,7 +49,7 @@ class ReadOnlyTest < ActiveRecord::TestCase post = Post.find(1) assert !post.comments.empty? assert !post.comments.any?(&:readonly?) - assert !post.comments.all.any?(&:readonly?) + assert !post.comments.to_a.any?(&:readonly?) assert post.comments.readonly(true).all?(&:readonly?) end @@ -71,13 +71,13 @@ class ReadOnlyTest < ActiveRecord::TestCase end def test_readonly_scoping - Post.where('1=1').scoped do + Post.where('1=1').scoping do assert !Post.find(1).readonly? assert Post.readonly(true).find(1).readonly? assert !Post.readonly(false).find(1).readonly? end - Post.joins(' ').scoped do + Post.joins(' ').scoping do assert !Post.find(1).readonly? assert Post.readonly.find(1).readonly? assert !Post.readonly(false).find(1).readonly? @@ -86,14 +86,14 @@ class ReadOnlyTest < ActiveRecord::TestCase # Oracle barfs on this because the join includes unqualified and # conflicting column names unless current_adapter?(:OracleAdapter) - Post.joins(', developers').scoped do + Post.joins(', developers').scoping do assert Post.find(1).readonly? assert Post.readonly.find(1).readonly? assert !Post.readonly(false).find(1).readonly? end end - Post.readonly(true).scoped do + Post.readonly(true).scoping do assert Post.find(1).readonly? assert Post.readonly.find(1).readonly? assert !Post.readonly(false).find(1).readonly? diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index 7dd5698dcf..588da68ec1 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -77,7 +77,7 @@ class ReflectionTest < ActiveRecord::TestCase end def test_reflection_klass_for_nested_class_name - reflection = MacroReflection.new(:company, nil, { :class_name => 'MyApplication::Business::Company' }, ActiveRecord::Base) + reflection = MacroReflection.new(:company, nil, nil, { :class_name => 'MyApplication::Business::Company' }, ActiveRecord::Base) assert_nothing_raised do assert_equal MyApplication::Business::Company, reflection.klass end @@ -85,15 +85,15 @@ class ReflectionTest < ActiveRecord::TestCase def test_aggregation_reflection reflection_for_address = AggregateReflection.new( - :composed_of, :address, { :mapping => [ %w(address_street street), %w(address_city city), %w(address_country country) ] }, Customer + :composed_of, :address, nil, { :mapping => [ %w(address_street street), %w(address_city city), %w(address_country country) ] }, Customer ) reflection_for_balance = AggregateReflection.new( - :composed_of, :balance, { :class_name => "Money", :mapping => %w(balance amount) }, Customer + :composed_of, :balance, nil, { :class_name => "Money", :mapping => %w(balance amount) }, Customer ) reflection_for_gps_location = AggregateReflection.new( - :composed_of, :gps_location, { }, Customer + :composed_of, :gps_location, nil, { }, Customer ) assert Customer.reflect_on_all_aggregations.include?(reflection_for_gps_location) @@ -117,7 +117,7 @@ class ReflectionTest < ActiveRecord::TestCase end def test_has_many_reflection - reflection_for_clients = AssociationReflection.new(:has_many, :clients, { :order => "id", :dependent => :destroy }, Firm) + reflection_for_clients = AssociationReflection.new(:has_many, :clients, nil, { :order => "id", :dependent => :destroy }, Firm) assert_equal reflection_for_clients, Firm.reflect_on_association(:clients) @@ -129,7 +129,7 @@ class ReflectionTest < ActiveRecord::TestCase end def test_has_one_reflection - reflection_for_account = AssociationReflection.new(:has_one, :account, { :foreign_key => "firm_id", :dependent => :destroy }, Firm) + reflection_for_account = AssociationReflection.new(:has_one, :account, nil, { :foreign_key => "firm_id", :dependent => :destroy }, Firm) assert_equal reflection_for_account, Firm.reflect_on_association(:account) assert_equal Account, Firm.reflect_on_association(:account).klass @@ -214,21 +214,25 @@ class ReflectionTest < ActiveRecord::TestCase assert_equal expected, actual end - def test_conditions + def test_scope_chain expected = [ - [{ :tags => { :name => 'Blue' } }], - [{ :taggings => { :comment => 'first' } }], - [{ :posts => { :title => ['misc post by bob', 'misc post by mary'] } }] + [Tagging.reflect_on_association(:tag).scope, Post.reflect_on_association(:first_blue_tags).scope], + [Post.reflect_on_association(:first_taggings).scope], + [Author.reflect_on_association(:misc_posts).scope] ] - actual = Author.reflect_on_association(:misc_post_first_blue_tags).conditions + actual = Author.reflect_on_association(:misc_post_first_blue_tags).scope_chain assert_equal expected, actual expected = [ - [{ :tags => { :name => 'Blue' } }, { :taggings => { :comment => 'first' } }, { :posts => { :title => ['misc post by bob', 'misc post by mary'] } }], + [ + Tagging.reflect_on_association(:blue_tag).scope, + Post.reflect_on_association(:first_blue_tags_2).scope, + Author.reflect_on_association(:misc_post_first_blue_tags_2).scope + ], [], [] ] - actual = Author.reflect_on_association(:misc_post_first_blue_tags_2).conditions + actual = Author.reflect_on_association(:misc_post_first_blue_tags_2).scope_chain assert_equal expected, actual end @@ -254,10 +258,10 @@ class ReflectionTest < ActiveRecord::TestCase end def test_association_primary_key_raises_when_missing_primary_key - reflection = ActiveRecord::Reflection::AssociationReflection.new(:fuu, :edge, {}, Author) + reflection = ActiveRecord::Reflection::AssociationReflection.new(:fuu, :edge, nil, {}, Author) assert_raises(ActiveRecord::UnknownPrimaryKey) { reflection.association_primary_key } - through = ActiveRecord::Reflection::ThroughReflection.new(:fuu, :edge, {}, Author) + through = ActiveRecord::Reflection::ThroughReflection.new(:fuu, :edge, nil, {}, Author) through.stubs(:source_reflection).returns(stub_everything(:options => {}, :class_name => 'Edge')) assert_raises(ActiveRecord::UnknownPrimaryKey) { through.association_primary_key } end @@ -268,7 +272,7 @@ class ReflectionTest < ActiveRecord::TestCase end def test_active_record_primary_key_raises_when_missing_primary_key - reflection = ActiveRecord::Reflection::AssociationReflection.new(:fuu, :author, {}, Edge) + reflection = ActiveRecord::Reflection::AssociationReflection.new(:fuu, :author, nil, {}, Edge) assert_raises(ActiveRecord::UnknownPrimaryKey) { reflection.active_record_primary_key } end @@ -286,32 +290,32 @@ class ReflectionTest < ActiveRecord::TestCase end def test_default_association_validation - assert AssociationReflection.new(:has_many, :clients, {}, Firm).validate? + assert AssociationReflection.new(:has_many, :clients, nil, {}, Firm).validate? - assert !AssociationReflection.new(:has_one, :client, {}, Firm).validate? - assert !AssociationReflection.new(:belongs_to, :client, {}, Firm).validate? - assert !AssociationReflection.new(:has_and_belongs_to_many, :clients, {}, Firm).validate? + assert !AssociationReflection.new(:has_one, :client, nil, {}, Firm).validate? + assert !AssociationReflection.new(:belongs_to, :client, nil, {}, Firm).validate? + assert !AssociationReflection.new(:has_and_belongs_to_many, :clients, nil, {}, Firm).validate? end def test_always_validate_association_if_explicit - assert AssociationReflection.new(:has_one, :client, { :validate => true }, Firm).validate? - assert AssociationReflection.new(:belongs_to, :client, { :validate => true }, Firm).validate? - assert AssociationReflection.new(:has_many, :clients, { :validate => true }, Firm).validate? - assert AssociationReflection.new(:has_and_belongs_to_many, :clients, { :validate => true }, Firm).validate? + assert AssociationReflection.new(:has_one, :client, nil, { :validate => true }, Firm).validate? + assert AssociationReflection.new(:belongs_to, :client, nil, { :validate => true }, Firm).validate? + assert AssociationReflection.new(:has_many, :clients, nil, { :validate => true }, Firm).validate? + assert AssociationReflection.new(:has_and_belongs_to_many, :clients, nil, { :validate => true }, Firm).validate? end def test_validate_association_if_autosave - assert AssociationReflection.new(:has_one, :client, { :autosave => true }, Firm).validate? - assert AssociationReflection.new(:belongs_to, :client, { :autosave => true }, Firm).validate? - assert AssociationReflection.new(:has_many, :clients, { :autosave => true }, Firm).validate? - assert AssociationReflection.new(:has_and_belongs_to_many, :clients, { :autosave => true }, Firm).validate? + assert AssociationReflection.new(:has_one, :client, nil, { :autosave => true }, Firm).validate? + assert AssociationReflection.new(:belongs_to, :client, nil, { :autosave => true }, Firm).validate? + assert AssociationReflection.new(:has_many, :clients, nil, { :autosave => true }, Firm).validate? + assert AssociationReflection.new(:has_and_belongs_to_many, :clients, nil, { :autosave => true }, Firm).validate? end def test_never_validate_association_if_explicit - assert !AssociationReflection.new(:has_one, :client, { :autosave => true, :validate => false }, Firm).validate? - assert !AssociationReflection.new(:belongs_to, :client, { :autosave => true, :validate => false }, Firm).validate? - assert !AssociationReflection.new(:has_many, :clients, { :autosave => true, :validate => false }, Firm).validate? - assert !AssociationReflection.new(:has_and_belongs_to_many, :clients, { :autosave => true, :validate => false }, Firm).validate? + assert !AssociationReflection.new(:has_one, :client, nil, { :autosave => true, :validate => false }, Firm).validate? + assert !AssociationReflection.new(:belongs_to, :client, nil, { :autosave => true, :validate => false }, Firm).validate? + assert !AssociationReflection.new(:has_many, :clients, nil, { :autosave => true, :validate => false }, Firm).validate? + assert !AssociationReflection.new(:has_and_belongs_to_many, :clients, nil, { :autosave => true, :validate => false }, Firm).validate? end def test_foreign_key @@ -319,16 +323,68 @@ class ReflectionTest < ActiveRecord::TestCase assert_equal "category_id", Post.reflect_on_association(:categorizations).foreign_key.to_s end - def test_through_reflection_conditions_do_not_modify_other_reflections - orig_conds = Post.reflect_on_association(:first_blue_tags_2).conditions.inspect - Author.reflect_on_association(:misc_post_first_blue_tags_2).conditions - assert_equal orig_conds, Post.reflect_on_association(:first_blue_tags_2).conditions.inspect + def test_through_reflection_scope_chain_does_not_modify_other_reflections + orig_conds = Post.reflect_on_association(:first_blue_tags_2).scope_chain.inspect + Author.reflect_on_association(:misc_post_first_blue_tags_2).scope_chain + assert_equal orig_conds, Post.reflect_on_association(:first_blue_tags_2).scope_chain.inspect end def test_symbol_for_class_name assert_equal Client, Firm.reflect_on_association(:unsorted_clients_with_symbol).klass end + def test_join_table + category = Struct.new(:table_name, :pluralize_table_names).new('categories', true) + product = Struct.new(:table_name, :pluralize_table_names).new('products', true) + + reflection = AssociationReflection.new(:has_and_belongs_to_many, :categories, nil, {}, product) + reflection.stubs(:klass).returns(category) + assert_equal 'categories_products', reflection.join_table + + reflection = AssociationReflection.new(:has_and_belongs_to_many, :products, nil, {}, category) + reflection.stubs(:klass).returns(product) + assert_equal 'categories_products', reflection.join_table + end + + def test_join_table_with_common_prefix + category = Struct.new(:table_name, :pluralize_table_names).new('catalog_categories', true) + product = Struct.new(:table_name, :pluralize_table_names).new('catalog_products', true) + + reflection = AssociationReflection.new(:has_and_belongs_to_many, :categories, nil, {}, product) + reflection.stubs(:klass).returns(category) + assert_equal 'catalog_categories_products', reflection.join_table + + reflection = AssociationReflection.new(:has_and_belongs_to_many, :products, nil, {}, category) + reflection.stubs(:klass).returns(product) + assert_equal 'catalog_categories_products', reflection.join_table + end + + def test_join_table_with_different_prefix + category = Struct.new(:table_name, :pluralize_table_names).new('catalog_categories', true) + page = Struct.new(:table_name, :pluralize_table_names).new('content_pages', true) + + reflection = AssociationReflection.new(:has_and_belongs_to_many, :categories, nil, {}, page) + reflection.stubs(:klass).returns(category) + assert_equal 'catalog_categories_content_pages', reflection.join_table + + reflection = AssociationReflection.new(:has_and_belongs_to_many, :pages, nil, {}, category) + reflection.stubs(:klass).returns(page) + assert_equal 'catalog_categories_content_pages', reflection.join_table + end + + def test_join_table_can_be_overridden + category = Struct.new(:table_name, :pluralize_table_names).new('categories', true) + product = Struct.new(:table_name, :pluralize_table_names).new('products', true) + + reflection = AssociationReflection.new(:has_and_belongs_to_many, :categories, nil, { :join_table => 'product_categories' }, product) + reflection.stubs(:klass).returns(category) + assert_equal 'product_categories', reflection.join_table + + reflection = AssociationReflection.new(:has_and_belongs_to_many, :products, nil, { :join_table => 'product_categories' }, category) + reflection.stubs(:klass).returns(product) + assert_equal 'product_categories', reflection.join_table + 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 cf367242f2..d318dab1e1 100644 --- a/activerecord/test/cases/relation_scoping_test.rb +++ b/activerecord/test/cases/relation_scoping_test.rb @@ -106,7 +106,7 @@ class RelationScopingTest < ActiveRecord::TestCase def test_scoped_find_include # with the include, will retrieve only developers for the given project scoped_developers = Developer.includes(:projects).scoping do - Developer.where('projects.id' => 2).all + Developer.where('projects.id' => 2).to_a end assert scoped_developers.include?(developers(:david)) assert !scoped_developers.include?(developers(:jamis)) @@ -115,7 +115,7 @@ class RelationScopingTest < ActiveRecord::TestCase def test_scoped_find_joins scoped_developers = Developer.joins('JOIN developers_projects ON id = developer_id').scoping do - Developer.where('developers_projects.project_id = 2').all + Developer.where('developers_projects.project_id = 2').to_a end assert scoped_developers.include?(developers(:david)) @@ -159,7 +159,7 @@ class RelationScopingTest < ActiveRecord::TestCase rescue end - assert !Developer.scoped.where_values.include?("name = 'Jamis'") + assert !Developer.all.where_values.include?("name = 'Jamis'") end end @@ -169,7 +169,7 @@ class NestedRelationScopingTest < ActiveRecord::TestCase def test_merge_options Developer.where('salary = 80000').scoping do Developer.limit(10).scoping do - devs = Developer.scoped + devs = Developer.all assert_match '(salary = 80000)', devs.to_sql assert_equal 10, devs.taken end @@ -312,7 +312,7 @@ class DefaultScopingTest < ActiveRecord::TestCase fixtures :developers, :posts def test_default_scope - expected = Developer.scoped(:order => 'salary DESC').all.collect { |dev| dev.salary } + expected = Developer.all.merge!(:order => 'salary DESC').to_a.collect { |dev| dev.salary } received = DeveloperOrderedBySalary.all.collect { |dev| dev.salary } assert_equal expected, received end @@ -362,31 +362,31 @@ class DefaultScopingTest < ActiveRecord::TestCase def test_default_scoping_with_threads 2.times do - Thread.new { assert DeveloperOrderedBySalary.scoped.to_sql.include?('salary DESC') }.join + Thread.new { assert DeveloperOrderedBySalary.all.to_sql.include?('salary DESC') }.join end end def test_default_scope_with_inheritance - wheres = InheritedPoorDeveloperCalledJamis.scoped.where_values_hash + wheres = InheritedPoorDeveloperCalledJamis.all.where_values_hash assert_equal "Jamis", wheres[:name] assert_equal 50000, wheres[:salary] end def test_default_scope_with_module_includes - wheres = ModuleIncludedPoorDeveloperCalledJamis.scoped.where_values_hash + wheres = ModuleIncludedPoorDeveloperCalledJamis.all.where_values_hash assert_equal "Jamis", wheres[:name] assert_equal 50000, wheres[:salary] end def test_default_scope_with_multiple_calls - wheres = MultiplePoorDeveloperCalledJamis.scoped.where_values_hash + wheres = MultiplePoorDeveloperCalledJamis.all.where_values_hash assert_equal "Jamis", wheres[:name] assert_equal 50000, wheres[:salary] end def test_scope_overwrites_default - expected = Developer.scoped(:order => 'salary DESC, name DESC').all.collect { |dev| dev.name } - received = DeveloperOrderedBySalary.by_name.all.collect { |dev| dev.name } + expected = Developer.all.merge!(:order => ' name DESC, salary DESC').to_a.collect { |dev| dev.name } + received = DeveloperOrderedBySalary.by_name.to_a.collect { |dev| dev.name } assert_equal expected, received end @@ -397,14 +397,14 @@ class DefaultScopingTest < ActiveRecord::TestCase end def test_order_after_reorder_combines_orders - expected = Developer.order('name DESC, id DESC').collect { |dev| [dev.name, dev.id] } + expected = Developer.order('id DESC, name DESC').collect { |dev| [dev.name, dev.id] } received = Developer.order('name ASC').reorder('name DESC').order('id DESC').collect { |dev| [dev.name, dev.id] } assert_equal expected, received end - def test_order_in_default_scope_should_prevail - expected = Developer.scoped(:order => 'salary desc').all.collect { |dev| dev.salary } - received = DeveloperOrderedBySalary.scoped(:order => 'salary').all.collect { |dev| dev.salary } + def test_order_in_default_scope_should_not_prevail + expected = Developer.all.merge!(:order => 'salary').to_a.collect { |dev| dev.salary } + received = DeveloperOrderedBySalary.all.merge!(:order => 'salary').to_a.collect { |dev| dev.salary } assert_equal expected, received end @@ -472,7 +472,7 @@ class DefaultScopingTest < ActiveRecord::TestCase end def test_default_scope_select_ignored_by_aggregations - assert_equal DeveloperWithSelect.all.count, DeveloperWithSelect.count + assert_equal DeveloperWithSelect.all.to_a.count, DeveloperWithSelect.count end def test_default_scope_select_ignored_by_grouped_aggregations @@ -508,10 +508,10 @@ class DefaultScopingTest < ActiveRecord::TestCase threads << Thread.new do Thread.current[:long_default_scope] = true - assert_equal 1, ThreadsafeDeveloper.all.count + assert_equal 1, ThreadsafeDeveloper.all.to_a.count end threads << Thread.new do - assert_equal 1, ThreadsafeDeveloper.all.count + assert_equal 1, ThreadsafeDeveloper.all.to_a.count end threads.each(&:join) end diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb index b7486196c5..6399111be6 100644 --- a/activerecord/test/cases/relation_test.rb +++ b/activerecord/test/cases/relation_test.rb @@ -196,11 +196,14 @@ module ActiveRecord end test 'extending!' do - mod = Module.new + mod, mod2 = Module.new, Module.new assert relation.extending!(mod).equal?(relation) - assert [mod], relation.extending_values + assert_equal [mod], relation.extending_values assert relation.is_a?(mod) + + relation.extending!(mod2) + assert_equal [mod, mod2], relation.extending_values end test 'extending! with empty args' do @@ -249,5 +252,9 @@ module ActiveRecord assert relation.merge!(where: :foo).equal?(relation) assert_equal [:foo], relation.where_values end + + test 'merge with a proc' do + assert_equal [:foo], relation.merge(-> { where(:foo) }).where_values + end end end diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 90367df5ee..684538940a 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -34,7 +34,7 @@ class RelationTest < ActiveRecord::TestCase end def test_bind_values - relation = Post.scoped + relation = Post.all assert_equal [], relation.bind_values relation2 = relation.bind 'foo' @@ -59,44 +59,44 @@ class RelationTest < ActiveRecord::TestCase end def test_scoped - topics = Topic.scoped + topics = Topic.all assert_kind_of ActiveRecord::Relation, topics assert_equal 4, topics.size end def test_to_json - assert_nothing_raised { Bird.scoped.to_json } - assert_nothing_raised { Bird.scoped.all.to_json } + assert_nothing_raised { Bird.all.to_json } + assert_nothing_raised { Bird.all.to_a.to_json } end def test_to_yaml - assert_nothing_raised { Bird.scoped.to_yaml } - assert_nothing_raised { Bird.scoped.all.to_yaml } + assert_nothing_raised { Bird.all.to_yaml } + assert_nothing_raised { Bird.all.to_a.to_yaml } end def test_to_xml - assert_nothing_raised { Bird.scoped.to_xml } - assert_nothing_raised { Bird.scoped.all.to_xml } + assert_nothing_raised { Bird.all.to_xml } + assert_nothing_raised { Bird.all.to_a.to_xml } end def test_scoped_all - topics = Topic.scoped.all + topics = Topic.all.to_a assert_kind_of Array, topics assert_no_queries { assert_equal 4, topics.size } end def test_loaded_all - topics = Topic.scoped + topics = Topic.all assert_queries(1) do - 2.times { assert_equal 4, topics.all.size } + 2.times { assert_equal 4, topics.to_a.size } end assert topics.loaded? end def test_scoped_first - topics = Topic.scoped.order('id ASC') + topics = Topic.all.order('id ASC') assert_queries(1) do 2.times { assert_equal "The First Topic", topics.first.title } @@ -106,10 +106,10 @@ class RelationTest < ActiveRecord::TestCase end def test_loaded_first - topics = Topic.scoped.order('id ASC') + topics = Topic.all.order('id ASC') assert_queries(1) do - topics.all # force load + topics.to_a # force load 2.times { assert_equal "The First Topic", topics.first.title } end @@ -117,7 +117,7 @@ class RelationTest < ActiveRecord::TestCase end def test_reload - topics = Topic.scoped + topics = Topic.all assert_queries(1) do 2.times { topics.to_a } @@ -165,13 +165,13 @@ class RelationTest < ActiveRecord::TestCase end def test_finding_with_order_concatenated - topics = Topic.order('author_name').order('title') + topics = Topic.order('title').order('author_name') assert_equal 4, topics.to_a.size assert_equal topics(:fourth).title, topics.first.title end def test_finding_with_reorder - topics = Topic.order('author_name').order('title').reorder('id').all + topics = Topic.order('author_name').order('title').reorder('id').to_a topics_titles = topics.map{ |t| t.title } assert_equal ['The First Topic', 'The Second Topic of the day', 'The Third Topic of the day', 'The Fourth Topic of the day'], topics_titles end @@ -218,14 +218,14 @@ class RelationTest < ActiveRecord::TestCase end def test_select_with_block - even_ids = Developer.scoped.select {|d| d.id % 2 == 0 }.map(&:id) + even_ids = Developer.all.select {|d| d.id % 2 == 0 }.map(&:id) assert_equal [2, 4, 6, 8, 10], even_ids.sort end def test_none assert_no_queries do assert_equal [], Developer.none - assert_equal [], Developer.scoped.none + assert_equal [], Developer.all.none end end @@ -294,7 +294,7 @@ class RelationTest < ActiveRecord::TestCase end def test_find_on_hash_conditions - assert_equal Topic.scoped(:where => {:approved => false}).all, Topic.where({ :approved => false }).to_a + assert_equal Topic.all.merge!(:where => {:approved => false}).to_a, Topic.where({ :approved => false }).to_a end def test_joins_with_string_array @@ -307,15 +307,15 @@ class RelationTest < ActiveRecord::TestCase end def test_scoped_responds_to_delegated_methods - relation = Topic.scoped + relation = Topic.all ["map", "uniq", "sort", "insert", "delete", "update"].each do |method| - assert_respond_to relation, method, "Topic.scoped should respond to #{method.inspect}" + assert_respond_to relation, method, "Topic.all should respond to #{method.inspect}" end end def test_respond_to_delegates_to_relation - relation = Topic.scoped + relation = Topic.all fake_arel = Struct.new(:responds) { def respond_to? method, access = false responds << [method, access] @@ -334,20 +334,20 @@ class RelationTest < ActiveRecord::TestCase end def test_respond_to_dynamic_finders - relation = Topic.scoped + relation = Topic.all ["find_by_title", "find_by_title_and_author_name", "find_or_create_by_title", "find_or_initialize_by_title_and_author_name"].each do |method| - assert_respond_to relation, method, "Topic.scoped should respond to #{method.inspect}" + assert_respond_to relation, method, "Topic.all should respond to #{method.inspect}" end end def test_respond_to_class_methods_and_scopes - assert Topic.scoped.respond_to?(:by_lifo) + assert Topic.all.respond_to?(:by_lifo) end def test_find_with_readonly_option - Developer.scoped.each { |d| assert !d.readonly? } - Developer.scoped.readonly.each { |d| assert d.readonly? } + Developer.all.each { |d| assert !d.readonly? } + Developer.all.readonly.each { |d| assert d.readonly? } end def test_eager_association_loading_of_stis_with_multiple_references @@ -396,7 +396,7 @@ class RelationTest < ActiveRecord::TestCase end assert_queries(2) do - posts = Post.scoped.includes(:comments).order('posts.id') + posts = Post.all.includes(:comments).order('posts.id') assert posts.first.comments.first end @@ -413,12 +413,12 @@ class RelationTest < ActiveRecord::TestCase end def test_default_scope_with_conditions_string - assert_equal Developer.where(name: 'David').map(&:id).sort, DeveloperCalledDavid.scoped.map(&:id).sort + assert_equal Developer.where(name: 'David').map(&:id).sort, DeveloperCalledDavid.all.map(&:id).sort assert_nil DeveloperCalledDavid.create!.name end def test_default_scope_with_conditions_hash - assert_equal Developer.where(name: 'Jamis').map(&:id).sort, DeveloperCalledJamis.scoped.map(&:id).sort + assert_equal Developer.where(name: 'Jamis').map(&:id).sort, DeveloperCalledJamis.all.map(&:id).sort assert_equal 'Jamis', DeveloperCalledJamis.create!.name end @@ -457,20 +457,20 @@ class RelationTest < ActiveRecord::TestCase assert_equal expected_taggings, author.taggings.uniq.sort_by { |t| t.id } end - authors = Author.scoped + authors = Author.all assert_equal david, authors.find_by_id_and_name(david.id, david.name) assert_equal david, authors.find_by_id_and_name!(david.id, david.name) end def test_dynamic_find_by_attributes_bang - author = Author.scoped.find_by_id!(authors(:david).id) + author = Author.all.find_by_id!(authors(:david).id) assert_equal "David", author.name - assert_raises(ActiveRecord::RecordNotFound) { Author.scoped.find_by_id_and_name!(20, 'invalid') } + assert_raises(ActiveRecord::RecordNotFound) { Author.all.find_by_id_and_name!(20, 'invalid') } end def test_find_id - authors = Author.scoped + authors = Author.all david = authors.find(authors(:david).id) assert_equal 'David', david.name @@ -493,14 +493,14 @@ class RelationTest < ActiveRecord::TestCase end def test_find_in_empty_array - authors = Author.scoped.where(:id => []) - assert_blank authors.all + authors = Author.all.where(:id => []) + assert_blank authors.to_a end def test_where_with_ar_object author = Author.first - authors = Author.scoped.where(:id => author) - assert_equal 1, authors.all.length + authors = Author.all.where(:id => author) + assert_equal 1, authors.to_a.length end def test_find_with_list_of_ar @@ -528,7 +528,7 @@ class RelationTest < ActiveRecord::TestCase relation = relation.where(:name => david.name) relation = relation.where(:name => 'Santiago') relation = relation.where(:id => david.id) - assert_equal [], relation.all + assert_equal [], relation.to_a end def test_multi_where_ands_queries @@ -547,7 +547,7 @@ class RelationTest < ActiveRecord::TestCase ].inject(Author.unscoped) do |memo, param| memo.where(param) end - assert_equal [], relation.all + assert_equal [], relation.to_a end def test_find_all_using_where_with_relation @@ -556,7 +556,7 @@ class RelationTest < ActiveRecord::TestCase # assert_queries(2) { assert_queries(1) { relation = Author.where(:id => Author.where(:id => david.id)) - assert_equal [david], relation.all + assert_equal [david], relation.to_a } end @@ -566,7 +566,7 @@ class RelationTest < ActiveRecord::TestCase # assert_queries(2) { assert_queries(1) { relation = Minivan.where(:minivan_id => Minivan.where(:name => cool_first.name)) - assert_equal [cool_first], relation.all + assert_equal [cool_first], relation.to_a } end @@ -577,7 +577,7 @@ class RelationTest < ActiveRecord::TestCase assert_queries(1) { relation = Author.where(:id => subquery) - assert_equal [david], relation.all + assert_equal [david], relation.to_a } assert_equal 0, subquery.select_values.size @@ -587,7 +587,7 @@ class RelationTest < ActiveRecord::TestCase david = authors(:david) assert_queries(1) { relation = Author.where(:id => Author.joins(:posts).where(:id => david.id)) - assert_equal [david], relation.all + assert_equal [david], relation.to_a } end @@ -596,7 +596,7 @@ class RelationTest < ActiveRecord::TestCase david = authors(:david) assert_queries(1) { relation = Author.where(:name => Author.where(:id => david.id).select(:name)) - assert_equal [david], relation.all + assert_equal [david], relation.to_a } end @@ -615,7 +615,7 @@ class RelationTest < ActiveRecord::TestCase end def test_last - authors = Author.scoped + authors = Author.all assert_equal authors(:bob), authors.last end @@ -668,10 +668,29 @@ class RelationTest < ActiveRecord::TestCase assert_equal [developers(:poor_jamis)], dev_with_count.to_a end + def test_relation_merging_with_arel_equalities_keeps_last_equality + devs = Developer.where(Developer.arel_table[:salary].eq(80000)).merge( + Developer.where(Developer.arel_table[:salary].eq(9000)) + ) + assert_equal [developers(:poor_jamis)], devs.to_a + end + + def test_relation_merging_with_arel_equalities_keeps_last_equality_with_non_attribute_left_hand + salary_attr = Developer.arel_table[:salary] + devs = Developer.where( + Arel::Nodes::NamedFunction.new('abs', [salary_attr]).eq(80000) + ).merge( + Developer.where( + Arel::Nodes::NamedFunction.new('abs', [salary_attr]).eq(9000) + ) + ) + assert_equal [developers(:poor_jamis)], devs.to_a + end + def test_relation_merging_with_eager_load relations = [] - relations << Post.order('comments.id DESC').merge(Post.eager_load(:last_comment)).merge(Post.scoped) - relations << Post.eager_load(:last_comment).merge(Post.order('comments.id DESC')).merge(Post.scoped) + relations << Post.order('comments.id DESC').merge(Post.eager_load(:last_comment)).merge(Post.all) + relations << Post.eager_load(:last_comment).merge(Post.order('comments.id DESC')).merge(Post.all) relations.each do |posts| post = posts.find { |p| p.id == 1 } @@ -685,7 +704,7 @@ class RelationTest < ActiveRecord::TestCase end def test_relation_merging_with_preload - [Post.scoped.merge(Post.preload(:author)), Post.preload(:author).merge(Post.scoped)].each do |posts| + [Post.all.merge(Post.preload(:author)), Post.preload(:author).merge(Post.all)].each do |posts| assert_queries(2) { assert posts.first.author } end end @@ -695,8 +714,16 @@ class RelationTest < ActiveRecord::TestCase assert_equal 1, comments.count end + def test_relation_merging_with_association + assert_queries(2) do # one for loading post, and another one merged query + post = Post.where(:body => 'Such a lovely day').first + comments = Comment.where(:body => 'Thank you for the welcome').merge(post.comments) + assert_equal 1, comments.count + end + end + def test_count - posts = Post.scoped + posts = Post.all assert_equal 11, posts.count assert_equal 11, posts.count(:all) @@ -707,7 +734,7 @@ class RelationTest < ActiveRecord::TestCase end def test_count_with_distinct - posts = Post.scoped + posts = Post.all assert_equal 3, posts.count(:comments_count, :distinct => true) assert_equal 11, posts.count(:comments_count, :distinct => false) @@ -718,7 +745,7 @@ class RelationTest < ActiveRecord::TestCase def test_count_explicit_columns Post.update_all(:comments_count => nil) - posts = Post.scoped + posts = Post.all assert_equal [0], posts.select('comments_count').where('id is not null').group('id').order('id').count.values.uniq assert_equal 0, posts.where('id is not null').select('comments_count').count @@ -730,13 +757,13 @@ class RelationTest < ActiveRecord::TestCase end def test_multiple_selects - post = Post.scoped.select('comments_count').select('title').order("id ASC").first + post = Post.all.select('comments_count').select('title').order("id ASC").first assert_equal "Welcome to the weblog", post.title assert_equal 2, post.comments_count end def test_size - posts = Post.scoped + posts = Post.all assert_queries(1) { assert_equal 11, posts.size } assert ! posts.loaded? @@ -782,7 +809,7 @@ class RelationTest < ActiveRecord::TestCase end def test_empty - posts = Post.scoped + posts = Post.all assert_queries(1) { assert_equal false, posts.empty? } assert ! posts.loaded? @@ -808,7 +835,7 @@ class RelationTest < ActiveRecord::TestCase end def test_any - posts = Post.scoped + posts = Post.all # This test was failing when run on its own (as opposed to running the entire suite). # The second line in the assert_queries block was causing visit_Arel_Attributes_Attribute @@ -830,7 +857,7 @@ class RelationTest < ActiveRecord::TestCase end def test_many - posts = Post.scoped + posts = Post.all assert_queries(2) do assert posts.many? # Uses COUNT() @@ -842,14 +869,14 @@ class RelationTest < ActiveRecord::TestCase end def test_many_with_limits - posts = Post.scoped + posts = Post.all assert posts.many? assert ! posts.limit(1).many? end def test_build - posts = Post.scoped + posts = Post.all post = posts.new assert_kind_of Post, post @@ -864,7 +891,7 @@ class RelationTest < ActiveRecord::TestCase end def test_create - birds = Bird.scoped + birds = Bird.all sparrow = birds.create assert_kind_of Bird, sparrow @@ -876,7 +903,7 @@ class RelationTest < ActiveRecord::TestCase end def test_create_bang - birds = Bird.scoped + birds = Bird.all assert_raises(ActiveRecord::RecordInvalid) { birds.create! } @@ -1018,24 +1045,24 @@ class RelationTest < ActiveRecord::TestCase def test_except relation = Post.where(:author_id => 1).order('id ASC').limit(1) - assert_equal [posts(:welcome)], relation.all + assert_equal [posts(:welcome)], relation.to_a author_posts = relation.except(:order, :limit) - assert_equal Post.where(:author_id => 1).all, author_posts.all + assert_equal Post.where(:author_id => 1).to_a, author_posts.to_a all_posts = relation.except(:where, :order, :limit) - assert_equal Post.all, all_posts.all + assert_equal Post.all, all_posts end def test_only relation = Post.where(:author_id => 1).order('id ASC').limit(1) - assert_equal [posts(:welcome)], relation.all + assert_equal [posts(:welcome)], relation.to_a author_posts = relation.only(:where) - assert_equal Post.where(:author_id => 1).all, author_posts.all + assert_equal Post.where(:author_id => 1).to_a, author_posts.to_a all_posts = relation.only(:limit) - assert_equal Post.limit(1).all.first, all_posts.first + assert_equal Post.limit(1).to_a.first, all_posts.first end def test_anonymous_extension @@ -1056,24 +1083,24 @@ class RelationTest < ActiveRecord::TestCase end def test_order_by_relation_attribute - assert_equal Post.order(Post.arel_table[:title]).all, Post.order("title").all + assert_equal Post.order(Post.arel_table[:title]).to_a, Post.order("title").to_a end def test_default_scope_order_with_scope_order - assert_equal 'zyke', CoolCar.order_using_new_style.limit(1).first.name - assert_equal 'zyke', FastCar.order_using_new_style.limit(1).first.name + assert_equal 'honda', CoolCar.order_using_new_style.limit(1).first.name + assert_equal 'honda', FastCar.order_using_new_style.limit(1).first.name end def test_order_using_scoping car1 = CoolCar.order('id DESC').scoping do - CoolCar.scoped(:order => 'id asc').first + CoolCar.all.merge!(:order => 'id asc').first end - assert_equal 'zyke', car1.name + assert_equal 'honda', car1.name car2 = FastCar.order('id DESC').scoping do - FastCar.scoped(:order => 'id asc').first + FastCar.all.merge!(:order => 'id asc').first end - assert_equal 'zyke', car2.name + assert_equal 'honda', car2.name end def test_unscoped_block_style @@ -1090,7 +1117,7 @@ class RelationTest < ActiveRecord::TestCase end def test_primary_key - assert_equal "id", Post.scoped.primary_key + assert_equal "id", Post.all.primary_key end def test_eager_loading_with_conditions_on_joins @@ -1114,6 +1141,10 @@ class RelationTest < ActiveRecord::TestCase assert_equal authors(:david), Author.order('id DESC , name DESC').last end + def test_update_all_with_blank_argument + assert_raises(ArgumentError) { Comment.update_all({}) } + end + def test_update_all_with_joins comments = Comment.joins(:post).where('posts.id' => posts(:welcome).id) count = comments.count @@ -1214,7 +1245,7 @@ class RelationTest < ActiveRecord::TestCase end def test_presence - topics = Topic.scoped + topics = Topic.all # the first query is triggered because there are no topics yet. assert_queries(1) { assert topics.present? } @@ -1248,7 +1279,7 @@ class RelationTest < ActiveRecord::TestCase end test "find_by returns nil if the record is missing" do - assert_equal nil, Post.scoped.find_by("1 = 0") + assert_equal nil, Post.all.find_by("1 = 0") end test "find_by doesn't have implicit ordering" do @@ -1273,7 +1304,73 @@ class RelationTest < ActiveRecord::TestCase test "find_by! raises RecordNotFound if the record is missing" do assert_raises(ActiveRecord::RecordNotFound) do - Post.scoped.find_by!("1 = 0") + Post.all.find_by!("1 = 0") + end + end + + test "loaded relations cannot be mutated by multi value methods" do + relation = Post.all + relation.to_a + + assert_raises(ActiveRecord::ImmutableRelation) do + relation.where! 'foo' + end + end + + test "loaded relations cannot be mutated by single value methods" do + relation = Post.all + relation.to_a + + assert_raises(ActiveRecord::ImmutableRelation) do + relation.limit! 5 + end + end + + test "loaded relations cannot be mutated by merge!" do + relation = Post.all + relation.to_a + + assert_raises(ActiveRecord::ImmutableRelation) do + relation.merge! where: 'foo' + end + end + + test "relations show the records in #inspect" do + relation = Post.limit(2) + assert_equal "#<ActiveRecord::Relation [#{Post.limit(2).map(&:inspect).join(', ')}]>", relation.inspect + end + + test "relations limit the records in #inspect at 10" do + relation = Post.limit(11) + assert_equal "#<ActiveRecord::Relation [#{Post.limit(10).map(&:inspect).join(', ')}, ...]>", relation.inspect + end + + test "already-loaded relations don't perform a new query in #inspect" do + relation = Post.limit(2) + relation.to_a + + expected = "#<ActiveRecord::Relation [#{Post.limit(2).map(&:inspect).join(', ')}]>" + + assert_no_queries do + assert_equal expected, relation.inspect + end + end + + test 'using a custom table affects the wheres' do + table_alias = Post.arel_table.alias('omg_posts') + + relation = ActiveRecord::Relation.new Post, table_alias + relation.where!(:foo => "bar") + + node = relation.arel.constraints.first.grep(Arel::Attributes::Attribute).first + assert_equal table_alias, node.relation + end + + test '#load' do + relation = Post.all + assert_queries(1) do + assert_equal relation, relation.load end + assert_no_queries { relation.to_a } end end diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index ab80dd1d6d..01dd25a9df 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -257,6 +257,13 @@ class SchemaDumperTest < ActiveRecord::TestCase end end + def test_schema_dump_includes_uuid_shorthand_definition + output = standard_dump + if %r{create_table "poistgresql_uuids"} =~ output + assert_match %r{t.uuid "guid"}, output + end + end + def test_schema_dump_includes_hstores_shorthand_definition output = standard_dump if %r{create_table "postgresql_hstores"} =~ output @@ -294,4 +301,36 @@ class SchemaDumperTest < ActiveRecord::TestCase output = standard_dump assert_match %r{create_table "subscribers", :id => false}, output end + + class CreateDogMigration < ActiveRecord::Migration + def up + create_table("dogs") do |t| + t.column :name, :string + end + add_index "dogs", [:name] + end + def down + drop_table("dogs") + end + end + + def test_schema_dump_with_table_name_prefix_and_suffix + original, $stdout = $stdout, StringIO.new + ActiveRecord::Base.table_name_prefix = 'foo_' + ActiveRecord::Base.table_name_suffix = '_bar' + + migration = CreateDogMigration.new + migration.migrate(:up) + + output = standard_dump + assert_no_match %r{create_table "foo_.+_bar"}, output + assert_no_match %r{create_index "foo_.+_bar"}, output + assert_no_match %r{create_table "schema_migrations"}, output + ensure + migration.migrate(:down) + + ActiveRecord::Base.table_name_suffix = ActiveRecord::Base.table_name_prefix = '' + $stdout = original + end + end diff --git a/activerecord/test/cases/serialization_test.rb b/activerecord/test/cases/serialization_test.rb index a4c065e667..10d8ccc711 100644 --- a/activerecord/test/cases/serialization_test.rb +++ b/activerecord/test/cases/serialization_test.rb @@ -51,4 +51,10 @@ class SerializationTest < ActiveRecord::TestCase assert_equal @contact_attributes[:awesome], contact.awesome, "For #{format}" end end + + def test_serialized_attributes_are_class_level_settings + topic = Topic.new + assert_raise(NoMethodError) { topic.serialized_attributes = [] } + assert_deprecated { topic.serialized_attributes } + end end diff --git a/activerecord/test/cases/session_store/sql_bypass_test.rb b/activerecord/test/cases/session_store/sql_bypass_test.rb index 6749d4ce98..b8cf4cf2cc 100644 --- a/activerecord/test/cases/session_store/sql_bypass_test.rb +++ b/activerecord/test/cases/session_store/sql_bypass_test.rb @@ -56,6 +56,20 @@ module ActiveRecord s.destroy assert_nil SqlBypass.find_by_session_id session_id end + + def test_data_column + SqlBypass.drop_table! if exists = Session.table_exists? + old, SqlBypass.data_column = SqlBypass.data_column, 'foo' + SqlBypass.create_table! + + session_id = 20 + SqlBypass.new(:data => 'hello', :session_id => session_id).save + assert_equal 'hello', SqlBypass.find_by_session_id(session_id).data + ensure + SqlBypass.drop_table! + SqlBypass.data_column = old + SqlBypass.create_table! if exists + end end end end diff --git a/activerecord/test/cases/store_test.rb b/activerecord/test/cases/store_test.rb index 79476ed2a4..fb0d116c08 100644 --- a/activerecord/test/cases/store_test.rb +++ b/activerecord/test/cases/store_test.rb @@ -13,7 +13,7 @@ class StoreTest < ActiveRecord::TestCase assert_equal 'black', @john.color assert_nil @john.homepage end - + test "writing store attributes through accessors" do @john.color = 'red' @john.homepage = '37signals.com' @@ -34,6 +34,11 @@ class StoreTest < ActiveRecord::TestCase assert @john.settings_changed? end + test "updating the store won't mark it as changed if an attribute isn't changed" do + @john.color = @john.color + assert !@john.settings_changed? + end + test "object initialization with not nullable column" do assert_equal true, @john.remember_login end @@ -111,4 +116,14 @@ class StoreTest < ActiveRecord::TestCase @john.is_a_good_guy = false assert_equal false, @john.is_a_good_guy end + + test "stored attributes are returned" do + assert_equal [:color, :homepage], Admin::User.stored_attributes[:settings] + end + + test "stores_attributes are class level settings" do + assert_raise(NoMethodError) { @john.stored_attributes = Hash.new } + assert_raise(NoMethodError) { @john.stored_attributes } + end + end diff --git a/activerecord/test/cases/tasks/database_tasks_test.rb b/activerecord/test/cases/tasks/database_tasks_test.rb new file mode 100644 index 0000000000..4f3489b7a5 --- /dev/null +++ b/activerecord/test/cases/tasks/database_tasks_test.rb @@ -0,0 +1,302 @@ +require 'cases/helper' + +module ActiveRecord + module DatabaseTasksSetupper + def setup + @mysql_tasks, @postgresql_tasks, @sqlite_tasks = stub, stub, stub + ActiveRecord::Tasks::MySQLDatabaseTasks.stubs(:new).returns @mysql_tasks + ActiveRecord::Tasks::PostgreSQLDatabaseTasks.stubs(:new).returns @postgresql_tasks + ActiveRecord::Tasks::SQLiteDatabaseTasks.stubs(:new).returns @sqlite_tasks + end + end + + ADAPTERS_TASKS = { + :mysql => :mysql_tasks, + :mysql2 => :mysql_tasks, + :postgresql => :postgresql_tasks, + :sqlite3 => :sqlite_tasks + } + + class DatabaseTasksRegisterTask < ActiveRecord::TestCase + def test_register_task + klazz = Class.new do + def initialize(*arguments); end + def structure_dump(filename); end + end + instance = klazz.new + + klazz.stubs(:new).returns instance + instance.expects(:structure_dump).with("awesome-file.sql") + + ActiveRecord::Tasks::DatabaseTasks.register_task(/foo/, klazz) + ActiveRecord::Tasks::DatabaseTasks.structure_dump({'adapter' => :foo}, "awesome-file.sql") + end + end + + class DatabaseTasksCreateTest < ActiveRecord::TestCase + include DatabaseTasksSetupper + + ADAPTERS_TASKS.each do |k, v| + define_method("test_#{k}_create") do + eval("@#{v}").expects(:create) + ActiveRecord::Tasks::DatabaseTasks.create 'adapter' => k + end + end + end + + class DatabaseTasksCreateAllTest < ActiveRecord::TestCase + def setup + @configurations = {'development' => {'database' => 'my-db'}} + + ActiveRecord::Base.stubs(:configurations).returns(@configurations) + end + + def test_ignores_configurations_without_databases + @configurations['development'].merge!('database' => nil) + + ActiveRecord::Tasks::DatabaseTasks.expects(:create).never + + ActiveRecord::Tasks::DatabaseTasks.create_all + end + + def test_ignores_remote_databases + @configurations['development'].merge!('host' => 'my.server.tld') + $stderr.stubs(:puts).returns(nil) + + ActiveRecord::Tasks::DatabaseTasks.expects(:create).never + + ActiveRecord::Tasks::DatabaseTasks.create_all + end + + def test_warning_for_remote_databases + @configurations['development'].merge!('host' => 'my.server.tld') + + $stderr.expects(:puts).with('This task only modifies local databases. my-db is on a remote host.') + + ActiveRecord::Tasks::DatabaseTasks.create_all + end + + def test_creates_configurations_with_local_ip + @configurations['development'].merge!('host' => '127.0.0.1') + + ActiveRecord::Tasks::DatabaseTasks.expects(:create) + + ActiveRecord::Tasks::DatabaseTasks.create_all + end + + def test_creates_configurations_with_local_host + @configurations['development'].merge!('host' => 'localhost') + + ActiveRecord::Tasks::DatabaseTasks.expects(:create) + + ActiveRecord::Tasks::DatabaseTasks.create_all + end + + def test_creates_configurations_with_blank_hosts + @configurations['development'].merge!('host' => nil) + + ActiveRecord::Tasks::DatabaseTasks.expects(:create) + + ActiveRecord::Tasks::DatabaseTasks.create_all + end + end + + class DatabaseTasksCreateCurrentTest < ActiveRecord::TestCase + def setup + @configurations = { + 'development' => {'database' => 'dev-db'}, + 'test' => {'database' => 'test-db'}, + 'production' => {'database' => 'prod-db'} + } + + ActiveRecord::Base.stubs(:configurations).returns(@configurations) + ActiveRecord::Base.stubs(:establish_connection).returns(true) + end + + def test_creates_current_environment_database + ActiveRecord::Tasks::DatabaseTasks.expects(:create). + with('database' => 'prod-db') + + ActiveRecord::Tasks::DatabaseTasks.create_current( + ActiveSupport::StringInquirer.new('production') + ) + end + + def test_creates_test_database_when_environment_is_database + ActiveRecord::Tasks::DatabaseTasks.expects(:create). + with('database' => 'dev-db') + ActiveRecord::Tasks::DatabaseTasks.expects(:create). + with('database' => 'test-db') + + ActiveRecord::Tasks::DatabaseTasks.create_current( + ActiveSupport::StringInquirer.new('development') + ) + end + + def test_establishes_connection_for_the_given_environment + ActiveRecord::Tasks::DatabaseTasks.stubs(:create).returns true + + ActiveRecord::Base.expects(:establish_connection).with('development') + + ActiveRecord::Tasks::DatabaseTasks.create_current( + ActiveSupport::StringInquirer.new('development') + ) + end + end + + class DatabaseTasksDropTest < ActiveRecord::TestCase + include DatabaseTasksSetupper + + ADAPTERS_TASKS.each do |k, v| + define_method("test_#{k}_drop") do + eval("@#{v}").expects(:drop) + ActiveRecord::Tasks::DatabaseTasks.drop 'adapter' => k + end + end + end + + class DatabaseTasksDropAllTest < ActiveRecord::TestCase + def setup + @configurations = {:development => {'database' => 'my-db'}} + + ActiveRecord::Base.stubs(:configurations).returns(@configurations) + end + + def test_ignores_configurations_without_databases + @configurations[:development].merge!('database' => nil) + + ActiveRecord::Tasks::DatabaseTasks.expects(:drop).never + + ActiveRecord::Tasks::DatabaseTasks.drop_all + end + + def test_ignores_remote_databases + @configurations[:development].merge!('host' => 'my.server.tld') + $stderr.stubs(:puts).returns(nil) + + ActiveRecord::Tasks::DatabaseTasks.expects(:drop).never + + ActiveRecord::Tasks::DatabaseTasks.drop_all + end + + def test_warning_for_remote_databases + @configurations[:development].merge!('host' => 'my.server.tld') + + $stderr.expects(:puts).with('This task only modifies local databases. my-db is on a remote host.') + + ActiveRecord::Tasks::DatabaseTasks.drop_all + end + + def test_creates_configurations_with_local_ip + @configurations[:development].merge!('host' => '127.0.0.1') + + ActiveRecord::Tasks::DatabaseTasks.expects(:drop) + + ActiveRecord::Tasks::DatabaseTasks.drop_all + end + + def test_creates_configurations_with_local_host + @configurations[:development].merge!('host' => 'localhost') + + ActiveRecord::Tasks::DatabaseTasks.expects(:drop) + + ActiveRecord::Tasks::DatabaseTasks.drop_all + end + + def test_creates_configurations_with_blank_hosts + @configurations[:development].merge!('host' => nil) + + ActiveRecord::Tasks::DatabaseTasks.expects(:drop) + + ActiveRecord::Tasks::DatabaseTasks.drop_all + end + end + + class DatabaseTasksDropCurrentTest < ActiveRecord::TestCase + def setup + @configurations = { + 'development' => {'database' => 'dev-db'}, + 'test' => {'database' => 'test-db'}, + 'production' => {'database' => 'prod-db'} + } + + ActiveRecord::Base.stubs(:configurations).returns(@configurations) + end + + def test_creates_current_environment_database + ActiveRecord::Tasks::DatabaseTasks.expects(:drop). + with('database' => 'prod-db') + + ActiveRecord::Tasks::DatabaseTasks.drop_current( + ActiveSupport::StringInquirer.new('production') + ) + end + + def test_creates_test_database_when_environment_is_database + ActiveRecord::Tasks::DatabaseTasks.expects(:drop). + with('database' => 'dev-db') + ActiveRecord::Tasks::DatabaseTasks.expects(:drop). + with('database' => 'test-db') + + ActiveRecord::Tasks::DatabaseTasks.drop_current( + ActiveSupport::StringInquirer.new('development') + ) + end + end + + + class DatabaseTasksPurgeTest < ActiveRecord::TestCase + include DatabaseTasksSetupper + + ADAPTERS_TASKS.each do |k, v| + define_method("test_#{k}_purge") do + eval("@#{v}").expects(:purge) + ActiveRecord::Tasks::DatabaseTasks.purge 'adapter' => k + end + end + end + + class DatabaseTasksCharsetTest < ActiveRecord::TestCase + include DatabaseTasksSetupper + + ADAPTERS_TASKS.each do |k, v| + define_method("test_#{k}_charset") do + eval("@#{v}").expects(:charset) + ActiveRecord::Tasks::DatabaseTasks.charset 'adapter' => k + end + end + end + + class DatabaseTasksCollationTest < ActiveRecord::TestCase + include DatabaseTasksSetupper + + ADAPTERS_TASKS.each do |k, v| + define_method("test_#{k}_collation") do + eval("@#{v}").expects(:collation) + ActiveRecord::Tasks::DatabaseTasks.collation 'adapter' => k + end + end + end + + class DatabaseTasksStructureDumpTest < ActiveRecord::TestCase + include DatabaseTasksSetupper + + ADAPTERS_TASKS.each do |k, v| + define_method("test_#{k}_structure_dump") do + eval("@#{v}").expects(:structure_dump).with("awesome-file.sql") + ActiveRecord::Tasks::DatabaseTasks.structure_dump({'adapter' => k}, "awesome-file.sql") + end + end + end + + class DatabaseTasksStructureLoadTest < ActiveRecord::TestCase + include DatabaseTasksSetupper + + ADAPTERS_TASKS.each do |k, v| + define_method("test_#{k}_structure_load") do + eval("@#{v}").expects(:structure_load).with("awesome-file.sql") + ActiveRecord::Tasks::DatabaseTasks.structure_load({'adapter' => k}, "awesome-file.sql") + end + end + end +end diff --git a/activerecord/test/cases/tasks/mysql_rake_test.rb b/activerecord/test/cases/tasks/mysql_rake_test.rb new file mode 100644 index 0000000000..b49561d858 --- /dev/null +++ b/activerecord/test/cases/tasks/mysql_rake_test.rb @@ -0,0 +1,268 @@ +require 'cases/helper' + +module ActiveRecord + class MysqlDBCreateTest < ActiveRecord::TestCase + def setup + @connection = stub(:create_database => true) + @configuration = { + 'adapter' => 'mysql', + 'database' => 'my-app-db' + } + + ActiveRecord::Base.stubs(:connection).returns(@connection) + ActiveRecord::Base.stubs(:establish_connection).returns(true) + end + + def test_establishes_connection_without_database + ActiveRecord::Base.expects(:establish_connection). + with('adapter' => 'mysql', 'database' => nil) + + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + + def test_creates_database_with_default_options + @connection.expects(:create_database). + with('my-app-db', {:charset => 'utf8', :collation => 'utf8_unicode_ci'}) + + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + + def test_creates_database_with_given_options + @connection.expects(:create_database). + with('my-app-db', {:charset => 'latin', :collation => 'latin_ci'}) + + ActiveRecord::Tasks::DatabaseTasks.create @configuration.merge( + 'charset' => 'latin', 'collation' => 'latin_ci' + ) + end + + def test_establishes_connection_to_database + ActiveRecord::Base.expects(:establish_connection).with(@configuration) + + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + end + + class MysqlDBCreateAsRootTest < ActiveRecord::TestCase + def setup + unless current_adapter?(:MysqlAdapter) + return skip("only tested on mysql") + end + + @connection = stub(:create_database => true, :execute => true) + @error = Mysql::Error.new "Invalid permissions" + @configuration = { + 'adapter' => 'mysql', + 'database' => 'my-app-db', + 'username' => 'pat', + 'password' => 'wossname' + } + + $stdin.stubs(:gets).returns("secret\n") + $stdout.stubs(:print).returns(nil) + @error.stubs(:errno).returns(1045) + ActiveRecord::Base.stubs(:connection).returns(@connection) + ActiveRecord::Base.stubs(:establish_connection).raises(@error).then. + returns(true) + end + + def test_root_password_is_requested + skip "only if mysql is available" unless defined?(::Mysql) + $stdin.expects(:gets).returns("secret\n") + + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + + def test_connection_established_as_root + ActiveRecord::Base.expects(:establish_connection).with({ + 'adapter' => 'mysql', + 'database' => nil, + 'username' => 'root', + 'password' => 'secret' + }) + + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + + def test_database_created_by_root + @connection.expects(:create_database). + with('my-app-db', :charset => 'utf8', :collation => 'utf8_unicode_ci') + + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + + def test_grant_privileges_for_normal_user + @connection.expects(:execute).with("GRANT ALL PRIVILEGES ON my-app-db.* TO 'pat'@'localhost' IDENTIFIED BY 'wossname' WITH GRANT OPTION;") + + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + + def test_connection_established_as_normal_user + ActiveRecord::Base.expects(:establish_connection).returns do + ActiveRecord::Base.expects(:establish_connection).with({ + 'adapter' => 'mysql', + 'database' => 'my-app-db', + 'username' => 'pat', + 'password' => 'secret' + }) + + raise @error + end + + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + + def test_sends_output_to_stderr_when_other_errors + @error.stubs(:errno).returns(42) + + $stderr.expects(:puts).at_least_once.returns(nil) + + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + end + + class MySQLDBDropTest < ActiveRecord::TestCase + def setup + @connection = stub(:drop_database => true) + @configuration = { + 'adapter' => 'mysql', + 'database' => 'my-app-db' + } + + ActiveRecord::Base.stubs(:connection).returns(@connection) + ActiveRecord::Base.stubs(:establish_connection).returns(true) + end + + def test_establishes_connection_to_mysql_database + ActiveRecord::Base.expects(:establish_connection).with @configuration + + ActiveRecord::Tasks::DatabaseTasks.drop @configuration + end + + def test_drops_database + @connection.expects(:drop_database).with('my-app-db') + + ActiveRecord::Tasks::DatabaseTasks.drop @configuration + end + end + + class MySQLPurgeTest < ActiveRecord::TestCase + def setup + @connection = stub(:recreate_database => true) + @configuration = { + 'adapter' => 'mysql', + 'database' => 'test-db' + } + + ActiveRecord::Base.stubs(:connection).returns(@connection) + ActiveRecord::Base.stubs(:establish_connection).returns(true) + end + + def test_establishes_connection_to_test_database + ActiveRecord::Base.expects(:establish_connection).with(:test) + + ActiveRecord::Tasks::DatabaseTasks.purge @configuration + end + + def test_recreates_database_with_the_default_options + @connection.expects(:recreate_database). + with('test-db', {:charset => 'utf8', :collation => 'utf8_unicode_ci'}) + + ActiveRecord::Tasks::DatabaseTasks.purge @configuration + end + + def test_recreates_database_with_the_given_options + @connection.expects(:recreate_database). + with('test-db', {:charset => 'latin', :collation => 'latin_ci'}) + + ActiveRecord::Tasks::DatabaseTasks.purge @configuration.merge( + 'charset' => 'latin', 'collation' => 'latin_ci' + ) + end + end + + class MysqlDBCharsetTest < ActiveRecord::TestCase + def setup + @connection = stub(:create_database => true) + @configuration = { + 'adapter' => 'mysql', + 'database' => 'my-app-db' + } + + ActiveRecord::Base.stubs(:connection).returns(@connection) + ActiveRecord::Base.stubs(:establish_connection).returns(true) + end + + def test_db_retrieves_charset + @connection.expects(:charset) + ActiveRecord::Tasks::DatabaseTasks.charset @configuration + end + end + + class MysqlDBCollationTest < ActiveRecord::TestCase + def setup + @connection = stub(:create_database => true) + @configuration = { + 'adapter' => 'mysql', + 'database' => 'my-app-db' + } + + ActiveRecord::Base.stubs(:connection).returns(@connection) + ActiveRecord::Base.stubs(:establish_connection).returns(true) + end + + def test_db_retrieves_collation + @connection.expects(:collation) + ActiveRecord::Tasks::DatabaseTasks.collation @configuration + end + end + + class MySQLStructureDumpTest < ActiveRecord::TestCase + def setup + @connection = stub(:structure_dump => true) + @configuration = { + 'adapter' => 'mysql', + 'database' => 'test-db' + } + + ActiveRecord::Base.stubs(:connection).returns(@connection) + ActiveRecord::Base.stubs(:establish_connection).returns(true) + end + + def test_structure_dump + filename = "awesome-file.sql" + ActiveRecord::Base.expects(:establish_connection).with(@configuration) + @connection.expects(:structure_dump) + + ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename) + assert File.exists?(filename) + ensure + FileUtils.rm(filename) + end + end + + class MySQLStructureLoadTest < ActiveRecord::TestCase + def setup + @connection = stub + @configuration = { + 'adapter' => 'mysql', + 'database' => 'test-db' + } + + ActiveRecord::Base.stubs(:connection).returns(@connection) + ActiveRecord::Base.stubs(:establish_connection).returns(true) + end + + def test_structure_load + filename = "awesome-file.sql" + ActiveRecord::Base.expects(:establish_connection).with(@configuration) + @connection.expects(:execute).twice + + open(filename, 'w') { |f| f.puts("SELECT CURDATE();") } + ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename) + ensure + FileUtils.rm(filename) + end + end + +end diff --git a/activerecord/test/cases/tasks/postgresql_rake_test.rb b/activerecord/test/cases/tasks/postgresql_rake_test.rb new file mode 100644 index 0000000000..62acd53003 --- /dev/null +++ b/activerecord/test/cases/tasks/postgresql_rake_test.rb @@ -0,0 +1,226 @@ +require 'cases/helper' + +module ActiveRecord + class PostgreSQLDBCreateTest < ActiveRecord::TestCase + def setup + @connection = stub(:create_database => true) + @configuration = { + 'adapter' => 'postgresql', + 'database' => 'my-app-db' + } + + ActiveRecord::Base.stubs(:connection).returns(@connection) + ActiveRecord::Base.stubs(:establish_connection).returns(true) + end + + def test_establishes_connection_to_postgresql_database + ActiveRecord::Base.expects(:establish_connection).with( + 'adapter' => 'postgresql', + 'database' => 'postgres', + 'schema_search_path' => 'public' + ) + + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + + def test_creates_database_with_default_encoding + @connection.expects(:create_database). + with('my-app-db', @configuration.merge('encoding' => 'utf8')) + + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + + def test_creates_database_with_given_encoding + @connection.expects(:create_database). + with('my-app-db', @configuration.merge('encoding' => 'latin')) + + ActiveRecord::Tasks::DatabaseTasks.create @configuration. + merge('encoding' => 'latin') + end + + def test_creates_database_with_given_collation_and_ctype + @connection.expects(:create_database). + with('my-app-db', @configuration.merge('encoding' => 'utf8', 'collation' => 'ja_JP.UTF8', 'ctype' => 'ja_JP.UTF8')) + + ActiveRecord::Tasks::DatabaseTasks.create @configuration. + merge('collation' => 'ja_JP.UTF8', 'ctype' => 'ja_JP.UTF8') + end + + def test_establishes_connection_to_new_database + ActiveRecord::Base.expects(:establish_connection).with(@configuration) + + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + + def test_db_create_with_error_prints_message + ActiveRecord::Base.stubs(:establish_connection).raises(Exception) + + $stderr.stubs(:puts).returns(true) + $stderr.expects(:puts). + with("Couldn't create database for #{@configuration.inspect}") + + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + end + + class PostgreSQLDBDropTest < ActiveRecord::TestCase + def setup + @connection = stub(:drop_database => true) + @configuration = { + 'adapter' => 'postgresql', + 'database' => 'my-app-db' + } + + ActiveRecord::Base.stubs(:connection).returns(@connection) + ActiveRecord::Base.stubs(:establish_connection).returns(true) + end + + def test_establishes_connection_to_postgresql_database + ActiveRecord::Base.expects(:establish_connection).with( + 'adapter' => 'postgresql', + 'database' => 'postgres', + 'schema_search_path' => 'public' + ) + + ActiveRecord::Tasks::DatabaseTasks.drop @configuration + end + + def test_drops_database + @connection.expects(:drop_database).with('my-app-db') + + ActiveRecord::Tasks::DatabaseTasks.drop @configuration + end + end + + class PostgreSQLPurgeTest < ActiveRecord::TestCase + def setup + @connection = stub(:create_database => true, :drop_database => true) + @configuration = { + 'adapter' => 'postgresql', + 'database' => 'my-app-db' + } + + ActiveRecord::Base.stubs(:connection).returns(@connection) + ActiveRecord::Base.stubs(:clear_active_connections!).returns(true) + ActiveRecord::Base.stubs(:establish_connection).returns(true) + end + + def test_clears_active_connections + ActiveRecord::Base.expects(:clear_active_connections!) + + ActiveRecord::Tasks::DatabaseTasks.purge @configuration + end + + def test_establishes_connection_to_postgresql_database + ActiveRecord::Base.expects(:establish_connection).with( + 'adapter' => 'postgresql', + 'database' => 'postgres', + 'schema_search_path' => 'public' + ) + + ActiveRecord::Tasks::DatabaseTasks.purge @configuration + end + + def test_drops_database + @connection.expects(:drop_database).with('my-app-db') + + ActiveRecord::Tasks::DatabaseTasks.purge @configuration + end + + def test_creates_database + @connection.expects(:create_database). + with('my-app-db', @configuration.merge('encoding' => 'utf8')) + + ActiveRecord::Tasks::DatabaseTasks.purge @configuration + end + + def test_establishes_connection + ActiveRecord::Base.expects(:establish_connection).with(@configuration) + + ActiveRecord::Tasks::DatabaseTasks.purge @configuration + end + end + + class PostgreSQLDBCharsetTest < ActiveRecord::TestCase + def setup + @connection = stub(:create_database => true) + @configuration = { + 'adapter' => 'postgresql', + 'database' => 'my-app-db' + } + + ActiveRecord::Base.stubs(:connection).returns(@connection) + ActiveRecord::Base.stubs(:establish_connection).returns(true) + end + + def test_db_retrieves_charset + @connection.expects(:encoding) + ActiveRecord::Tasks::DatabaseTasks.charset @configuration + end + end + + class PostgreSQLDBCollationTest < ActiveRecord::TestCase + def setup + @connection = stub(:create_database => true) + @configuration = { + 'adapter' => 'postgresql', + 'database' => 'my-app-db' + } + + ActiveRecord::Base.stubs(:connection).returns(@connection) + ActiveRecord::Base.stubs(:establish_connection).returns(true) + end + + def test_db_retrieves_collation + @connection.expects(:collation) + ActiveRecord::Tasks::DatabaseTasks.collation @configuration + end + end + + class PostgreSQLStructureDumpTest < ActiveRecord::TestCase + def setup + @connection = stub(:structure_dump => true) + @configuration = { + 'adapter' => 'postgresql', + 'database' => 'my-app-db' + } + + ActiveRecord::Base.stubs(:connection).returns(@connection) + ActiveRecord::Base.stubs(:establish_connection).returns(true) + Kernel.stubs(:system) + end + + def test_structure_dump + filename = "awesome-file.sql" + Kernel.expects(:system).with("pg_dump -i -s -x -O -f #{filename} my-app-db").returns(true) + @connection.expects(:schema_search_path).returns("foo") + + ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename) + assert File.exists?(filename) + ensure + FileUtils.rm(filename) + end + end + + class PostgreSQLStructureLoadTest < ActiveRecord::TestCase + def setup + @connection = stub + @configuration = { + 'adapter' => 'postgresql', + 'database' => 'my-app-db' + } + + ActiveRecord::Base.stubs(:connection).returns(@connection) + ActiveRecord::Base.stubs(:establish_connection).returns(true) + Kernel.stubs(:system) + end + + def test_structure_dump + filename = "awesome-file.sql" + Kernel.expects(:system).with("psql -f #{filename} my-app-db") + + ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename) + end + end + +end diff --git a/activerecord/test/cases/tasks/sqlite_rake_test.rb b/activerecord/test/cases/tasks/sqlite_rake_test.rb new file mode 100644 index 0000000000..7209c0f14d --- /dev/null +++ b/activerecord/test/cases/tasks/sqlite_rake_test.rb @@ -0,0 +1,191 @@ +require 'cases/helper' +require 'pathname' + +module ActiveRecord + class SqliteDBCreateTest < ActiveRecord::TestCase + def setup + @database = 'db_create.sqlite3' + @connection = stub :connection + @configuration = { + 'adapter' => 'sqlite3', + 'database' => @database + } + + File.stubs(:exist?).returns(false) + ActiveRecord::Base.stubs(:connection).returns(@connection) + ActiveRecord::Base.stubs(:establish_connection).returns(true) + end + + def test_db_checks_database_exists + File.expects(:exist?).with(@database).returns(false) + + ActiveRecord::Tasks::DatabaseTasks.create @configuration, '/rails/root' + end + + def test_db_create_when_file_exists + File.stubs(:exist?).returns(true) + + $stderr.expects(:puts).with("#{@database} already exists") + + ActiveRecord::Tasks::DatabaseTasks.create @configuration, '/rails/root' + end + + def test_db_create_with_file_does_nothing + File.stubs(:exist?).returns(true) + $stderr.stubs(:puts).returns(nil) + + ActiveRecord::Base.expects(:establish_connection).never + + ActiveRecord::Tasks::DatabaseTasks.create @configuration, '/rails/root' + end + + def test_db_create_establishes_a_connection + ActiveRecord::Base.expects(:establish_connection).with(@configuration) + + ActiveRecord::Tasks::DatabaseTasks.create @configuration, '/rails/root' + end + + def test_db_create_with_error_prints_message + ActiveRecord::Base.stubs(:establish_connection).raises(Exception) + + $stderr.stubs(:puts).returns(true) + $stderr.expects(:puts). + with("Couldn't create database for #{@configuration.inspect}") + + ActiveRecord::Tasks::DatabaseTasks.create @configuration, '/rails/root' + end + end + + class SqliteDBDropTest < ActiveRecord::TestCase + def setup + @database = "db_create.sqlite3" + @path = stub(:to_s => '/absolute/path', :absolute? => true) + @configuration = { + 'adapter' => 'sqlite3', + 'database' => @database + } + + Pathname.stubs(:new).returns(@path) + File.stubs(:join).returns('/former/relative/path') + FileUtils.stubs(:rm).returns(true) + end + + def test_creates_path_from_database + Pathname.expects(:new).with(@database).returns(@path) + + ActiveRecord::Tasks::DatabaseTasks.drop @configuration, '/rails/root' + end + + def test_removes_file_with_absolute_path + File.stubs(:exist?).returns(true) + @path.stubs(:absolute?).returns(true) + + FileUtils.expects(:rm).with('/absolute/path') + + ActiveRecord::Tasks::DatabaseTasks.drop @configuration, '/rails/root' + end + + def test_generates_absolute_path_with_given_root + @path.stubs(:absolute?).returns(false) + + File.expects(:join).with('/rails/root', @path). + returns('/former/relative/path') + + ActiveRecord::Tasks::DatabaseTasks.drop @configuration, '/rails/root' + end + + def test_removes_file_with_relative_path + File.stubs(:exist?).returns(true) + @path.stubs(:absolute?).returns(false) + + FileUtils.expects(:rm).with('/former/relative/path') + + ActiveRecord::Tasks::DatabaseTasks.drop @configuration, '/rails/root' + end + end + + class SqliteDBCharsetTest < ActiveRecord::TestCase + def setup + @database = 'db_create.sqlite3' + @connection = stub :connection + @configuration = { + 'adapter' => 'sqlite3', + 'database' => @database + } + + File.stubs(:exist?).returns(false) + ActiveRecord::Base.stubs(:connection).returns(@connection) + ActiveRecord::Base.stubs(:establish_connection).returns(true) + end + + def test_db_retrieves_charset + @connection.expects(:encoding) + ActiveRecord::Tasks::DatabaseTasks.charset @configuration, '/rails/root' + end + end + + class SqliteDBCollationTest < ActiveRecord::TestCase + def setup + @database = 'db_create.sqlite3' + @connection = stub :connection + @configuration = { + 'adapter' => 'sqlite3', + 'database' => @database + } + + File.stubs(:exist?).returns(false) + ActiveRecord::Base.stubs(:connection).returns(@connection) + ActiveRecord::Base.stubs(:establish_connection).returns(true) + end + + def test_db_retrieves_collation + assert_raise NoMethodError do + ActiveRecord::Tasks::DatabaseTasks.collation @configuration, '/rails/root' + end + end + end + + class SqliteStructureDumpTest < ActiveRecord::TestCase + def setup + @database = "db_create.sqlite3" + @configuration = { + 'adapter' => 'sqlite3', + 'database' => @database + } + end + + def test_structure_dump + dbfile = @database + filename = "awesome-file.sql" + + ActiveRecord::Tasks::DatabaseTasks.structure_dump @configuration, filename, '/rails/root' + assert File.exists?(dbfile) + assert File.exists?(filename) + ensure + FileUtils.rm_f(filename) + FileUtils.rm_f(dbfile) + end + end + + class SqliteStructureLoadTest < ActiveRecord::TestCase + def setup + @database = "db_create.sqlite3" + @configuration = { + 'adapter' => 'sqlite3', + 'database' => @database + } + end + + def test_structure_load + dbfile = @database + filename = "awesome-file.sql" + + open(filename, 'w') { |f| f.puts("select datetime('now', 'localtime');") } + ActiveRecord::Tasks::DatabaseTasks.structure_load @configuration, filename, '/rails/root' + assert File.exists?(dbfile) + ensure + FileUtils.rm_f(filename) + FileUtils.rm_f(dbfile) + end + end +end diff --git a/activerecord/test/cases/test_case.rb b/activerecord/test/cases/test_case.rb index 94a13d386c..f3f7054794 100644 --- a/activerecord/test/cases/test_case.rb +++ b/activerecord/test/cases/test_case.rb @@ -1,4 +1,3 @@ -require 'active_support/deprecation' ActiveSupport::Deprecation.silence do require 'active_record/test_case' end diff --git a/activerecord/test/cases/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb index 447aa29ffe..bb034848e1 100644 --- a/activerecord/test/cases/timestamp_test.rb +++ b/activerecord/test/cases/timestamp_test.rb @@ -11,7 +11,7 @@ class TimestampTest < ActiveRecord::TestCase def setup @developer = Developer.first - @developer.update_attribute(:updated_at, Time.now.prev_month) + @developer.update_columns(updated_at: Time.now.prev_month) @previously_updated_at = @developer.updated_at end @@ -114,9 +114,12 @@ class TimestampTest < ActiveRecord::TestCase end def test_saving_a_record_with_a_belongs_to_that_specifies_touching_a_specific_attribute_the_parent_should_update_that_attribute - Pet.belongs_to :owner, :touch => :happy_at + klass = Class.new(ActiveRecord::Base) do + def self.name; 'Pet'; end + belongs_to :owner, :touch => :happy_at + end - pet = Pet.first + pet = klass.first owner = pet.owner previously_owner_happy_at = owner.happy_at @@ -124,42 +127,41 @@ class TimestampTest < ActiveRecord::TestCase pet.save assert_not_equal previously_owner_happy_at, pet.owner.happy_at - ensure - Pet.belongs_to :owner, :touch => true end def test_touching_a_record_with_a_belongs_to_that_uses_a_counter_cache_should_update_the_parent - Pet.belongs_to :owner, :counter_cache => :use_count, :touch => true + klass = Class.new(ActiveRecord::Base) do + def self.name; 'Pet'; end + belongs_to :owner, :counter_cache => :use_count, :touch => true + end - pet = Pet.first + pet = klass.first owner = pet.owner - owner.update_attribute(:happy_at, 3.days.ago) + owner.update_columns(happy_at: 3.days.ago) previously_owner_updated_at = owner.updated_at pet.name = "I'm a parrot" pet.save assert_not_equal previously_owner_updated_at, pet.owner.updated_at - ensure - Pet.belongs_to :owner, :touch => true end def test_touching_a_record_touches_parent_record_and_grandparent_record - Toy.belongs_to :pet, :touch => true - Pet.belongs_to :owner, :touch => true + klass = Class.new(ActiveRecord::Base) do + def self.name; 'Toy'; end + belongs_to :pet, :touch => true + end - toy = Toy.first + toy = klass.first pet = toy.pet owner = pet.owner time = 3.days.ago - owner.update_column(:updated_at, time) + owner.update_columns(updated_at: time) toy.touch owner.reload assert_not_equal time, owner.updated_at - ensure - Toy.belongs_to :pet end def test_timestamp_attributes_for_create diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb index a9ccd00fac..0d0de455b3 100644 --- a/activerecord/test/cases/transactions_test.rb +++ b/activerecord/test/cases/transactions_test.rb @@ -91,18 +91,14 @@ class TransactionTest < ActiveRecord::TestCase end def test_raising_exception_in_callback_rollbacks_in_save - add_exception_raising_after_save_callback_to_topic - - begin - @first.approved = true - @first.save - flunk - rescue => e - assert_equal "Make the transaction rollback", e.message - assert !Topic.find(1).approved? - ensure - remove_exception_raising_after_save_callback_to_topic + def @first.after_save_for_transaction + raise 'Make the transaction rollback' end + + @first.approved = true + e = assert_raises(RuntimeError) { @first.save } + assert_equal "Make the transaction rollback", e.message + assert !Topic.find(1).approved? end def test_update_attributes_should_rollback_on_failure @@ -125,85 +121,85 @@ class TransactionTest < ActiveRecord::TestCase end def test_cancellation_from_before_destroy_rollbacks_in_destroy - add_cancelling_before_destroy_with_db_side_effect_to_topic - begin - nbooks_before_destroy = Book.count - status = @first.destroy - assert !status - assert_nothing_raised(ActiveRecord::RecordNotFound) { @first.reload } - assert_equal nbooks_before_destroy, Book.count - ensure - remove_cancelling_before_destroy_with_db_side_effect_to_topic - end + add_cancelling_before_destroy_with_db_side_effect_to_topic @first + nbooks_before_destroy = Book.count + status = @first.destroy + assert !status + @first.reload + assert_equal nbooks_before_destroy, Book.count end - def test_cancellation_from_before_filters_rollbacks_in_save - %w(validation save).each do |filter| - send("add_cancelling_before_#{filter}_with_db_side_effect_to_topic") - begin - nbooks_before_save = Book.count - original_author_name = @first.author_name - @first.author_name += '_this_should_not_end_up_in_the_db' - status = @first.save - assert !status - assert_equal original_author_name, @first.reload.author_name - assert_equal nbooks_before_save, Book.count - ensure - send("remove_cancelling_before_#{filter}_with_db_side_effect_to_topic") - end + %w(validation save).each do |filter| + define_method("test_cancellation_from_before_filters_rollbacks_in_#{filter}") do + send("add_cancelling_before_#{filter}_with_db_side_effect_to_topic", @first) + nbooks_before_save = Book.count + original_author_name = @first.author_name + @first.author_name += '_this_should_not_end_up_in_the_db' + status = @first.save + assert !status + assert_equal original_author_name, @first.reload.author_name + assert_equal nbooks_before_save, Book.count end - end - def test_cancellation_from_before_filters_rollbacks_in_save! - %w(validation save).each do |filter| - send("add_cancelling_before_#{filter}_with_db_side_effect_to_topic") + define_method("test_cancellation_from_before_filters_rollbacks_in_#{filter}!") do + send("add_cancelling_before_#{filter}_with_db_side_effect_to_topic", @first) + nbooks_before_save = Book.count + original_author_name = @first.author_name + @first.author_name += '_this_should_not_end_up_in_the_db' + begin - nbooks_before_save = Book.count - original_author_name = @first.author_name - @first.author_name += '_this_should_not_end_up_in_the_db' @first.save! - flunk - rescue - assert_equal original_author_name, @first.reload.author_name - assert_equal nbooks_before_save, Book.count - ensure - send("remove_cancelling_before_#{filter}_with_db_side_effect_to_topic") + rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved end + + assert_equal original_author_name, @first.reload.author_name + assert_equal nbooks_before_save, Book.count end end def test_callback_rollback_in_create - new_topic = Topic.new( - :title => "A new topic", - :author_name => "Ben", - :author_email_address => "ben@example.com", - :written_on => "2003-07-16t15:28:11.2233+01:00", - :last_read => "2004-04-15", - :bonus_time => "2005-01-30t15:28:00.00+01:00", - :content => "Have a nice day", - :approved => false) + topic = Class.new(Topic) { + def after_create_for_transaction + raise 'Make the transaction rollback' + end + } + + new_topic = topic.new(:title => "A new topic", + :author_name => "Ben", + :author_email_address => "ben@example.com", + :written_on => "2003-07-16t15:28:11.2233+01:00", + :last_read => "2004-04-15", + :bonus_time => "2005-01-30t15:28:00.00+01:00", + :content => "Have a nice day", + :approved => false) + new_record_snapshot = !new_topic.persisted? id_present = new_topic.has_attribute?(Topic.primary_key) id_snapshot = new_topic.id # Make sure the second save gets the after_create callback called. 2.times do - begin - add_exception_raising_after_create_callback_to_topic - new_topic.approved = true - new_topic.save - flunk - rescue => e - assert_equal "Make the transaction rollback", e.message - assert_equal new_record_snapshot, !new_topic.persisted?, "The topic should have its old persisted value" - assert_equal id_snapshot, new_topic.id, "The topic should have its old id" - assert_equal id_present, new_topic.has_attribute?(Topic.primary_key) - ensure - remove_exception_raising_after_create_callback_to_topic - end + new_topic.approved = true + e = assert_raises(RuntimeError) { new_topic.save } + assert_equal "Make the transaction rollback", e.message + assert_equal new_record_snapshot, !new_topic.persisted?, "The topic should have its old persisted value" + assert_equal id_snapshot, new_topic.id, "The topic should have its old id" + assert_equal id_present, new_topic.has_attribute?(Topic.primary_key) end end + def test_callback_rollback_in_create_with_record_invalid_exception + topic = Class.new(Topic) { + def after_create_for_transaction + raise ActiveRecord::RecordInvalid.new(Author.new) + end + } + + new_topic = topic.create(:title => "A new topic") + assert !new_topic.persisted?, "The topic should not be persisted" + assert_nil new_topic.id, "The topic should not have an ID" + end + def test_nested_explicit_transactions Topic.transaction do Topic.transaction do @@ -461,62 +457,16 @@ class TransactionTest < ActiveRecord::TestCase end private - def define_callback_method(callback_method) - define_method(callback_method) do - self.history << [callback_method, :method] - end - end - def add_exception_raising_after_save_callback_to_topic - Topic.class_eval <<-eoruby, __FILE__, __LINE__ + 1 - remove_method(:after_save_for_transaction) - def after_save_for_transaction - raise 'Make the transaction rollback' - end - eoruby - end - - def remove_exception_raising_after_save_callback_to_topic - Topic.class_eval <<-eoruby, __FILE__, __LINE__ + 1 - remove_method :after_save_for_transaction - def after_save_for_transaction; end - eoruby - end - - def add_exception_raising_after_create_callback_to_topic - Topic.class_eval <<-eoruby, __FILE__, __LINE__ + 1 - remove_method(:after_create_for_transaction) - def after_create_for_transaction - raise 'Make the transaction rollback' - end - eoruby - end - - def remove_exception_raising_after_create_callback_to_topic - Topic.class_eval <<-eoruby, __FILE__, __LINE__ + 1 - remove_method :after_create_for_transaction - def after_create_for_transaction; end - eoruby - end - - %w(validation save destroy).each do |filter| - define_method("add_cancelling_before_#{filter}_with_db_side_effect_to_topic") do - Topic.class_eval <<-eoruby, __FILE__, __LINE__ + 1 - remove_method :before_#{filter}_for_transaction - def before_#{filter}_for_transaction - Book.create - false - end - eoruby - end - - define_method("remove_cancelling_before_#{filter}_with_db_side_effect_to_topic") do - Topic.class_eval <<-eoruby, __FILE__, __LINE__ + 1 - remove_method :before_#{filter}_for_transaction - def before_#{filter}_for_transaction; end - eoruby + %w(validation save destroy).each do |filter| + define_method("add_cancelling_before_#{filter}_with_db_side_effect_to_topic") do |topic| + meta = class << topic; self; end + meta.send("define_method", "before_#{filter}_for_transaction") do + Book.create + false end end + end end class TransactionsWithTransactionalFixturesTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/validations/presence_validation_test.rb b/activerecord/test/cases/validations/presence_validation_test.rb new file mode 100644 index 0000000000..cd9175f454 --- /dev/null +++ b/activerecord/test/cases/validations/presence_validation_test.rb @@ -0,0 +1,44 @@ +# encoding: utf-8 +require "cases/helper" +require 'models/man' +require 'models/face' +require 'models/interest' + +class PresenceValidationTest < ActiveRecord::TestCase + class Boy < Man; end + + repair_validations(Boy) + + def test_validates_presence_of_non_association + Boy.validates_presence_of(:name) + b = Boy.new + assert b.invalid? + + b.name = "Alex" + assert b.valid? + end + + def test_validates_presence_of_has_one_marked_for_destruction + Boy.validates_presence_of(:face) + b = Boy.new + f = Face.new + b.face = f + assert b.valid? + + f.mark_for_destruction + assert b.invalid? + end + + def test_validates_presence_of_has_many_marked_for_destruction + Boy.validates_presence_of(:interests) + b = Boy.new + b.interests << [i1 = Interest.new, i2 = Interest.new] + assert b.valid? + + i1.mark_for_destruction + assert b.valid? + + i2.mark_for_destruction + assert b.invalid? + end +end diff --git a/activerecord/test/cases/validations/uniqueness_validation_test.rb b/activerecord/test/cases/validations/uniqueness_validation_test.rb index c173ee9a15..46212e49b6 100644 --- a/activerecord/test/cases/validations/uniqueness_validation_test.rb +++ b/activerecord/test/cases/validations/uniqueness_validation_test.rb @@ -22,6 +22,14 @@ end class Thaumaturgist < IneptWizard end +class ReplyTitle; end + +class ReplyWithTitleObject < Reply + validates_uniqueness_of :content, :scope => :title + + def title; ReplyTitle.new; end +end + class UniquenessValidationTest < ActiveRecord::TestCase fixtures :topics, 'warehouse-things', :developers @@ -104,6 +112,14 @@ class UniquenessValidationTest < ActiveRecord::TestCase assert !r2.valid?, "Saving r2 first time" end + def test_validate_uniqueness_with_composed_attribute_scope + r1 = ReplyWithTitleObject.create "title" => "r1", "content" => "hello world" + assert r1.valid?, "Saving r1" + + r2 = ReplyWithTitleObject.create "title" => "r1", "content" => "hello world" + assert !r2.valid?, "Saving r2 first time" + end + def test_validate_uniqueness_with_object_arg Reply.validates_uniqueness_of(:topic) @@ -189,7 +205,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase assert t_utf8.save, "Should save t_utf8 as unique" # If database hasn't UTF-8 character set, this test fails - if Topic.scoped(:select => 'LOWER(title) AS title').find(t_utf8).title == "я тоже уникальный!" + if Topic.all.merge!(:select => 'LOWER(title) AS title').find(t_utf8).title == "я тоже уникальный!" t2_utf8 = Topic.new("title" => "я тоже УНИКАЛЬНЫЙ!") assert !t2_utf8.valid?, "Shouldn't be valid" assert !t2_utf8.save, "Shouldn't save t2_utf8 as unique" diff --git a/activerecord/test/cases/xml_serialization_test.rb b/activerecord/test/cases/xml_serialization_test.rb index 12373333b0..7249ae9e4d 100644 --- a/activerecord/test/cases/xml_serialization_test.rb +++ b/activerecord/test/cases/xml_serialization_test.rb @@ -74,33 +74,88 @@ end class DefaultXmlSerializationTest < ActiveRecord::TestCase def setup - @xml = Contact.new(:name => 'aaron stack', :age => 25, :avatar => 'binarydata', :created_at => Time.utc(2006, 8, 1), :awesome => false, :preferences => { :gem => 'ruby' }).to_xml + @contact = Contact.new( + :name => 'aaron stack', + :age => 25, + :avatar => 'binarydata', + :created_at => Time.utc(2006, 8, 1), + :awesome => false, + :preferences => { :gem => 'ruby' } + ) end def test_should_serialize_string - assert_match %r{<name>aaron stack</name>}, @xml + assert_match %r{<name>aaron stack</name>}, @contact.to_xml end def test_should_serialize_integer - assert_match %r{<age type="integer">25</age>}, @xml + assert_match %r{<age type="integer">25</age>}, @contact.to_xml end def test_should_serialize_binary - assert_match %r{YmluYXJ5ZGF0YQ==\n</avatar>}, @xml - assert_match %r{<avatar(.*)(type="binary")}, @xml - assert_match %r{<avatar(.*)(encoding="base64")}, @xml + xml = @contact.to_xml + assert_match %r{YmluYXJ5ZGF0YQ==\n</avatar>}, xml + assert_match %r{<avatar(.*)(type="binary")}, xml + assert_match %r{<avatar(.*)(encoding="base64")}, xml end def test_should_serialize_datetime - assert_match %r{<created-at type=\"dateTime\">2006-08-01T00:00:00Z</created-at>}, @xml + assert_match %r{<created-at type=\"dateTime\">2006-08-01T00:00:00Z</created-at>}, @contact.to_xml end def test_should_serialize_boolean - assert_match %r{<awesome type=\"boolean\">false</awesome>}, @xml + assert_match %r{<awesome type=\"boolean\">false</awesome>}, @contact.to_xml end def test_should_serialize_hash - assert_match %r{<preferences>\s*<gem>ruby</gem>\s*</preferences>}m, @xml + assert_match %r{<preferences>\s*<gem>ruby</gem>\s*</preferences>}m, @contact.to_xml + end + + def test_uses_serializable_hash_with_only_option + def @contact.serializable_hash(options=nil) + super(only: %w(name)) + end + + xml = @contact.to_xml + assert_match %r{<name>aaron stack</name>}, xml + assert_no_match %r{age}, xml + assert_no_match %r{awesome}, xml + end + + def test_uses_serializable_hash_with_except_option + def @contact.serializable_hash(options=nil) + super(except: %w(age)) + end + + xml = @contact.to_xml + assert_match %r{<name>aaron stack</name>}, xml + assert_match %r{<awesome type=\"boolean\">false</awesome>}, xml + assert_no_match %r{age}, xml + end + + def test_does_not_include_inheritance_column_from_sti + @contact = ContactSti.new(@contact.attributes) + assert_equal 'ContactSti', @contact.type + + xml = @contact.to_xml + assert_match %r{<name>aaron stack</name>}, xml + assert_no_match %r{<type}, xml + assert_no_match %r{ContactSti}, xml + end + + def test_serializable_hash_with_default_except_option_and_excluding_inheritance_column_from_sti + @contact = ContactSti.new(@contact.attributes) + assert_equal 'ContactSti', @contact.type + + def @contact.serializable_hash(options={}) + super({ except: %w(age) }.merge!(options)) + end + + xml = @contact.to_xml + assert_match %r{<name>aaron stack</name>}, xml + assert_no_match %r{age}, xml + assert_no_match %r{<type}, xml + assert_no_match %r{ContactSti}, xml end end diff --git a/activerecord/test/config.example.yml b/activerecord/test/config.example.yml index f450efd839..479b8c050d 100644 --- a/activerecord/test/config.example.yml +++ b/activerecord/test/config.example.yml @@ -1,5 +1,7 @@ default_connection: <%= defined?(JRUBY_VERSION) ? 'jdbcsqlite3' : 'sqlite3' %> +with_manual_interventions: false + connections: jdbcderby: arunit: activerecord_unittest diff --git a/activerecord/test/fixtures/friendships.yml b/activerecord/test/fixtures/friendships.yml new file mode 100644 index 0000000000..1ee09175bf --- /dev/null +++ b/activerecord/test/fixtures/friendships.yml @@ -0,0 +1,4 @@ +Connection 1: + id: 1 + person_id: 1 + friend_id: 2
\ No newline at end of file diff --git a/activerecord/test/fixtures/people.yml b/activerecord/test/fixtures/people.yml index 123673a2af..e640a38f1f 100644 --- a/activerecord/test/fixtures/people.yml +++ b/activerecord/test/fixtures/people.yml @@ -4,15 +4,18 @@ michael: primary_contact_id: 2 number1_fan_id: 3 gender: M + followers_count: 1 david: id: 2 first_name: David primary_contact_id: 3 number1_fan_id: 1 gender: M + followers_count: 1 susan: id: 3 first_name: Susan primary_contact_id: 2 number1_fan_id: 1 gender: F + followers_count: 1 diff --git a/activerecord/test/fixtures/reserved_words/distincts_selects.yml b/activerecord/test/fixtures/reserved_words/distinct_select.yml index 90e8c95fef..d96779ade4 100644 --- a/activerecord/test/fixtures/reserved_words/distincts_selects.yml +++ b/activerecord/test/fixtures/reserved_words/distinct_select.yml @@ -1,11 +1,11 @@ -distincts_selects1: +distinct_select1: distinct_id: 1 select_id: 1 -distincts_selects2: +distinct_select2: distinct_id: 1 select_id: 2 -distincts_selects3: +distinct_select3: distinct_id: 2 select_id: 3 diff --git a/activerecord/test/fixtures/topics.yml b/activerecord/test/fixtures/topics.yml index 93f48aedc4..2b042bd135 100644 --- a/activerecord/test/fixtures/topics.yml +++ b/activerecord/test/fixtures/topics.yml @@ -25,7 +25,7 @@ third: id: 3 title: The Third Topic of the day author_name: Carl - written_on: 2005-07-15t15:28:00.0099+01:00 + written_on: 2012-08-12t20:24:22.129346+00:00 content: I'm a troll approved: true replies_count: 1 diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb index 14444a0092..77f4a2ec87 100644 --- a/activerecord/test/models/author.rb +++ b/activerecord/test/models/author.rb @@ -1,12 +1,12 @@ class Author < ActiveRecord::Base has_many :posts has_many :very_special_comments, :through => :posts - has_many :posts_with_comments, :include => :comments, :class_name => "Post" - has_many :popular_grouped_posts, :include => :comments, :class_name => "Post", :group => "type", :having => "SUM(comments_count) > 1", :select => "type" - has_many :posts_with_comments_sorted_by_comment_id, :include => :comments, :class_name => "Post", :order => 'comments.id' - has_many :posts_sorted_by_id_limited, :class_name => "Post", :order => 'posts.id', :limit => 1 - has_many :posts_with_categories, :include => :categories, :class_name => "Post" - has_many :posts_with_comments_and_categories, :include => [ :comments, :categories ], :order => "posts.id", :class_name => "Post" + has_many :posts_with_comments, -> { includes(:comments) }, :class_name => "Post" + has_many :popular_grouped_posts, -> { includes(:comments).group("type").having("SUM(comments_count) > 1").select("type") }, :class_name => "Post" + has_many :posts_with_comments_sorted_by_comment_id, -> { includes(:comments).order('comments.id') }, :class_name => "Post" + has_many :posts_sorted_by_id_limited, -> { order('posts.id').limit(1) }, :class_name => "Post" + has_many :posts_with_categories, -> { includes(:categories) }, :class_name => "Post" + has_many :posts_with_comments_and_categories, -> { includes(:comments, :categories).order("posts.id") }, :class_name => "Post" has_many :posts_containing_the_letter_a, :class_name => "Post" has_many :posts_with_extension, :class_name => "Post" do #, :extend => ProxyTestExtension def testing_proxy_owner @@ -19,28 +19,28 @@ class Author < ActiveRecord::Base proxy_target end end - has_one :post_about_thinking, :class_name => 'Post', :conditions => "posts.title like '%thinking%'" - has_one :post_about_thinking_with_last_comment, :class_name => 'Post', :conditions => "posts.title like '%thinking%'", :include => :last_comment + has_one :post_about_thinking, -> { where("posts.title like '%thinking%'") }, :class_name => 'Post' + has_one :post_about_thinking_with_last_comment, -> { where("posts.title like '%thinking%'").includes(:last_comment) }, :class_name => 'Post' has_many :comments, :through => :posts has_many :comments_containing_the_letter_e, :through => :posts, :source => :comments - has_many :comments_with_order_and_conditions, :through => :posts, :source => :comments, :order => 'comments.body', :conditions => "comments.body like 'Thank%'" - has_many :comments_with_include, :through => :posts, :source => :comments, :include => :post + has_many :comments_with_order_and_conditions, -> { order('comments.body').where("comments.body like 'Thank%'") }, :through => :posts, :source => :comments + has_many :comments_with_include, -> { includes(:post) }, :through => :posts, :source => :comments has_many :first_posts - has_many :comments_on_first_posts, :through => :first_posts, :source => :comments, :order => 'posts.id desc, comments.id asc' + has_many :comments_on_first_posts, -> { order('posts.id desc, comments.id asc') }, :through => :first_posts, :source => :comments has_one :first_post - has_one :comment_on_first_post, :through => :first_post, :source => :comments, :order => 'posts.id desc, comments.id asc' + has_one :comment_on_first_post, -> { order('posts.id desc, comments.id asc') }, :through => :first_post, :source => :comments - has_many :thinking_posts, :class_name => 'Post', :conditions => { :title => 'So I was thinking' }, :dependent => :delete_all - has_many :welcome_posts, :class_name => 'Post', :conditions => { :title => 'Welcome to the weblog' } + has_many :thinking_posts, -> { where(:title => 'So I was thinking') }, :dependent => :delete_all, :class_name => 'Post' + has_many :welcome_posts, -> { where(:title => 'Welcome to the weblog') }, :class_name => 'Post' - has_many :comments_desc, :through => :posts, :source => :comments, :order => 'comments.id DESC' - has_many :limited_comments, :through => :posts, :source => :comments, :limit => 1 + has_many :comments_desc, -> { order('comments.id DESC') }, :through => :posts, :source => :comments + has_many :limited_comments, -> { limit(1) }, :through => :posts, :source => :comments has_many :funky_comments, :through => :posts, :source => :comments - has_many :ordered_uniq_comments, :through => :posts, :source => :comments, :uniq => true, :order => 'comments.id' - has_many :ordered_uniq_comments_desc, :through => :posts, :source => :comments, :uniq => true, :order => 'comments.id DESC' - has_many :readonly_comments, :through => :posts, :source => :comments, :readonly => true + has_many :ordered_uniq_comments, -> { uniq.order('comments.id') }, :through => :posts, :source => :comments + has_many :ordered_uniq_comments_desc, -> { uniq.order('comments.id DESC') }, :through => :posts, :source => :comments + has_many :readonly_comments, -> { readonly }, :through => :posts, :source => :comments has_many :special_posts has_many :special_post_comments, :through => :special_posts, :source => :comments @@ -48,16 +48,15 @@ class Author < ActiveRecord::Base has_many :sti_posts, :class_name => 'StiPost' has_many :sti_post_comments, :through => :sti_posts, :source => :comments - has_many :special_nonexistant_posts, :class_name => "SpecialPost", :conditions => "posts.body = 'nonexistant'" - has_many :special_nonexistant_post_comments, :through => :special_nonexistant_posts, :source => :comments, :conditions => { 'comments.post_id' => 0 } + has_many :special_nonexistant_posts, -> { where("posts.body = 'nonexistant'") }, :class_name => "SpecialPost" + has_many :special_nonexistant_post_comments, -> { where('comments.post_id' => 0) }, :through => :special_nonexistant_posts, :source => :comments has_many :nonexistant_comments, :through => :posts - has_many :hello_posts, :class_name => "Post", :conditions => "posts.body = 'hello'" + has_many :hello_posts, -> { where "posts.body = 'hello'" }, :class_name => "Post" has_many :hello_post_comments, :through => :hello_posts, :source => :comments - has_many :posts_with_no_comments, :class_name => 'Post', :conditions => { 'comments.id' => nil }, :include => :comments + has_many :posts_with_no_comments, -> { where('comments.id' => nil).includes(:comments) }, :class_name => 'Post' - has_many :hello_posts_with_hash_conditions, :class_name => "Post", -:conditions => {:body => 'hello'} + has_many :hello_posts_with_hash_conditions, -> { where(:body => 'hello') }, :class_name => "Post" has_many :hello_post_comments_with_hash_conditions, :through => :hello_posts_with_hash_conditions, :source => :comments @@ -84,29 +83,31 @@ class Author < ActiveRecord::Base has_many :special_categories, :through => :special_categorizations, :source => :category has_one :special_category, :through => :special_categorizations, :source => :category - has_many :categories_like_general, :through => :categorizations, :source => :category, :class_name => 'Category', :conditions => { :name => 'General' } + has_many :categories_like_general, -> { where(:name => 'General') }, :through => :categorizations, :source => :category, :class_name => 'Category' has_many :categorized_posts, :through => :categorizations, :source => :post - has_many :unique_categorized_posts, :through => :categorizations, :source => :post, :uniq => true + has_many :unique_categorized_posts, -> { uniq }, :through => :categorizations, :source => :post has_many :nothings, :through => :kateggorisatons, :class_name => 'Category' has_many :author_favorites - has_many :favorite_authors, :through => :author_favorites, :order => 'name' + has_many :favorite_authors, -> { order('name') }, :through => :author_favorites - has_many :tagging, :through => :posts has_many :taggings, :through => :posts + has_many :taggings_2, :through => :posts, :source => :tagging has_many :tags, :through => :posts - has_many :similar_posts, :through => :tags, :source => :tagged_posts, :uniq => true - has_many :distinct_tags, :through => :posts, :source => :tags, :select => "DISTINCT tags.*", :order => "tags.name" has_many :post_categories, :through => :posts, :source => :categories has_many :tagging_tags, :through => :taggings, :source => :tag + + has_many :similar_posts, -> { uniq }, :through => :tags, :source => :tagged_posts + has_many :distinct_tags, -> { select("DISTINCT tags.*").order("tags.name") }, :through => :posts, :source => :tags + has_many :tags_with_primary_key, :through => :posts has_many :books has_many :subscriptions, :through => :books - has_many :subscribers, :through => :subscriptions, :order => "subscribers.nick" # through has_many :through (on through reflection) - has_many :distinct_subscribers, :through => :subscriptions, :source => :subscriber, :select => "DISTINCT subscribers.*", :order => "subscribers.nick" + has_many :subscribers, -> { order("subscribers.nick") }, :through => :subscriptions + has_many :distinct_subscribers, -> { select("DISTINCT subscribers.*").order("subscribers.nick") }, :through => :subscriptions, :source => :subscriber has_one :essay, :primary_key => :name, :as => :writer has_one :essay_category, :through => :essay, :source => :category @@ -130,12 +131,11 @@ class Author < ActiveRecord::Base has_many :category_post_comments, :through => :categories, :source => :post_comments - has_many :misc_posts, :class_name => 'Post', - :conditions => { :posts => { :title => ['misc post by bob', 'misc post by mary'] } } + has_many :misc_posts, -> { where(:posts => { :title => ['misc post by bob', 'misc post by mary'] }) }, :class_name => 'Post' has_many :misc_post_first_blue_tags, :through => :misc_posts, :source => :first_blue_tags - 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 :misc_post_first_blue_tags_2, -> { where(:posts => { :title => ['misc post by bob', 'misc post by mary'] }) }, + :through => :posts, :source => :first_blue_tags_2 has_many :posts_with_default_include, :class_name => 'PostWithDefaultInclude' has_many :comments_on_posts_with_default_include, :through => :posts_with_default_include, :source => :comments diff --git a/activerecord/test/models/book.rb b/activerecord/test/models/book.rb index d27d0af77c..ce81a37966 100644 --- a/activerecord/test/models/book.rb +++ b/activerecord/test/models/book.rb @@ -2,7 +2,7 @@ class Book < ActiveRecord::Base has_many :authors has_many :citations, :foreign_key => 'book1_id' - has_many :references, :through => :citations, :source => :reference_of, :uniq => true + has_many :references, -> { uniq }, :through => :citations, :source => :reference_of has_many :subscriptions has_many :subscribers, :through => :subscriptions diff --git a/activerecord/test/models/bulb.rb b/activerecord/test/models/bulb.rb index 640e57555d..0dc2fdd8ae 100644 --- a/activerecord/test/models/bulb.rb +++ b/activerecord/test/models/bulb.rb @@ -8,7 +8,7 @@ class Bulb < ActiveRecord::Base after_initialize :record_scope_after_initialize def record_scope_after_initialize - @scope_after_initialize = self.class.scoped + @scope_after_initialize = self.class.all end after_initialize :record_attributes_after_initialize diff --git a/activerecord/test/models/car.rb b/activerecord/test/models/car.rb index b4bc0ad5fa..ac42f444e1 100644 --- a/activerecord/test/models/car.rb +++ b/activerecord/test/models/car.rb @@ -1,11 +1,11 @@ class Car < ActiveRecord::Base has_many :bulbs - has_many :foo_bulbs, :class_name => "Bulb", :conditions => { :name => 'foo' } - has_many :frickinawesome_bulbs, :class_name => "Bulb", :conditions => { :frickinawesome => true } + has_many :foo_bulbs, -> { where(:name => 'foo') }, :class_name => "Bulb" + has_many :frickinawesome_bulbs, -> { where :frickinawesome => true }, :class_name => "Bulb" has_one :bulb - has_one :frickinawesome_bulb, :class_name => "Bulb", :conditions => { :frickinawesome => true } + has_one :frickinawesome_bulb, -> { where :frickinawesome => true }, :class_name => "Bulb" has_many :tyres has_many :engines, :dependent => :destroy diff --git a/activerecord/test/models/category.rb b/activerecord/test/models/category.rb index ab3139680c..f8c8ebb70c 100644 --- a/activerecord/test/models/category.rb +++ b/activerecord/test/models/category.rb @@ -2,20 +2,20 @@ class Category < ActiveRecord::Base has_and_belongs_to_many :posts has_and_belongs_to_many :special_posts, :class_name => "Post" has_and_belongs_to_many :other_posts, :class_name => "Post" - has_and_belongs_to_many :posts_with_authors_sorted_by_author_id, :class_name => "Post", :include => :authors, :order => "authors.id" + has_and_belongs_to_many :posts_with_authors_sorted_by_author_id, -> { includes(:authors).order("authors.id") }, :class_name => "Post" - has_and_belongs_to_many(:select_testing_posts, + has_and_belongs_to_many :select_testing_posts, + -> { select 'posts.*, 1 as correctness_marker' }, :class_name => 'Post', :foreign_key => 'category_id', - :association_foreign_key => 'post_id', - :select => 'posts.*, 1 as correctness_marker') + :association_foreign_key => 'post_id' has_and_belongs_to_many :post_with_conditions, - :class_name => 'Post', - :conditions => { :title => 'Yet Another Testing Title' } + -> { where :title => 'Yet Another Testing Title' }, + :class_name => 'Post' - has_and_belongs_to_many :popular_grouped_posts, :class_name => "Post", :group => "posts.type", :having => "sum(comments.post_id) > 2", :include => :comments - has_and_belongs_to_many :posts_grouped_by_title, :class_name => "Post", :group => "title", :select => "title" + has_and_belongs_to_many :popular_grouped_posts, -> { group("posts.type").having("sum(comments.post_id) > 2").includes(:comments) }, :class_name => "Post" + has_and_belongs_to_many :posts_grouped_by_title, -> { group("title").select("title") }, :class_name => "Post" def self.what_are_you 'a category...' @@ -25,7 +25,7 @@ class Category < ActiveRecord::Base has_many :post_comments, :through => :posts, :source => :comments has_many :authors, :through => :categorizations - has_many :authors_with_select, :through => :categorizations, :source => :author, :select => 'authors.*, categorizations.post_id' + has_many :authors_with_select, -> { select 'authors.*, categorizations.post_id' }, :through => :categorizations, :source => :author scope :general, -> { where(:name => 'General') } end diff --git a/activerecord/test/models/comment.rb b/activerecord/test/models/comment.rb index 3e9f1b0635..4b2015fe01 100644 --- a/activerecord/test/models/comment.rb +++ b/activerecord/test/models/comment.rb @@ -4,7 +4,7 @@ class Comment < ActiveRecord::Base scope :not_again, -> { where("comments.body NOT LIKE '%again%'") } scope :for_first_post, -> { where(:post_id => 1) } scope :for_first_author, -> { joins(:post).where("posts.author_id" => 1) } - scope :created, -> { scoped } + scope :created, -> { all } belongs_to :post, :counter_cache => true has_many :ratings @@ -19,13 +19,13 @@ class Comment < ActiveRecord::Base end def self.search_by_type(q) - self.scoped(:where => ["#{QUOTED_TYPE} = ?", q]).all + where("#{QUOTED_TYPE} = ?", q) end def self.all_as_method all end - scope :all_as_scope, -> { scoped } + scope :all_as_scope, -> { all } end class SpecialComment < Comment diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb index 7b993d5a2c..75f38d275c 100644 --- a/activerecord/test/models/company.rb +++ b/activerecord/test/models/company.rb @@ -36,60 +36,64 @@ module Namespaced end class Firm < Company - has_many :clients, :order => "id", :dependent => :destroy, :counter_sql => - "SELECT COUNT(*) FROM companies WHERE firm_id = 1 " + - "AND (#{QUOTED_TYPE} = 'Client' OR #{QUOTED_TYPE} = 'SpecialClient' OR #{QUOTED_TYPE} = 'VerySpecialClient' )", - :before_remove => :log_before_remove, - :after_remove => :log_after_remove + ActiveSupport::Deprecation.silence do + has_many :clients, -> { order "id" }, :dependent => :destroy, :counter_sql => + "SELECT COUNT(*) FROM companies WHERE firm_id = 1 " + + "AND (#{QUOTED_TYPE} = 'Client' OR #{QUOTED_TYPE} = 'SpecialClient' OR #{QUOTED_TYPE} = 'VerySpecialClient' )", + :before_remove => :log_before_remove, + :after_remove => :log_after_remove + end has_many :unsorted_clients, :class_name => "Client" has_many :unsorted_clients_with_symbol, :class_name => :Client - has_many :clients_sorted_desc, :class_name => "Client", :order => "id DESC" - has_many :clients_of_firm, :foreign_key => "client_of", :class_name => "Client", :order => "id" - has_many :clients_ordered_by_name, :order => "name", :class_name => "Client" + has_many :clients_sorted_desc, -> { order "id DESC" }, :class_name => "Client" + has_many :clients_of_firm, -> { order "id" }, :foreign_key => "client_of", :class_name => "Client" + has_many :clients_ordered_by_name, -> { order "name" }, :class_name => "Client" has_many :unvalidated_clients_of_firm, :foreign_key => "client_of", :class_name => "Client", :validate => false - has_many :dependent_clients_of_firm, :foreign_key => "client_of", :class_name => "Client", :order => "id", :dependent => :destroy - has_many :exclusively_dependent_clients_of_firm, :foreign_key => "client_of", :class_name => "Client", :order => "id", :dependent => :delete_all - has_many :limited_clients, :class_name => "Client", :limit => 1 - has_many :clients_with_interpolated_conditions, :class_name => "Client", :conditions => proc { "rating > #{rating}" } - has_many :clients_like_ms, :conditions => "name = 'Microsoft'", :class_name => "Client", :order => "id" - has_many :clients_like_ms_with_hash_conditions, :conditions => { :name => 'Microsoft' }, :class_name => "Client", :order => "id" - has_many :clients_using_sql, :class_name => "Client", :finder_sql => proc { "SELECT * FROM companies WHERE client_of = #{id}" } - has_many :clients_using_counter_sql, :class_name => "Client", - :finder_sql => proc { "SELECT * FROM companies WHERE client_of = #{id} " }, - :counter_sql => proc { "SELECT COUNT(*) FROM companies WHERE client_of = #{id}" } - has_many :clients_using_zero_counter_sql, :class_name => "Client", - :finder_sql => proc { "SELECT * FROM companies WHERE client_of = #{id}" }, - :counter_sql => proc { "SELECT 0 FROM companies WHERE client_of = #{id}" } - has_many :no_clients_using_counter_sql, :class_name => "Client", - :finder_sql => 'SELECT * FROM companies WHERE client_of = 1000', - :counter_sql => 'SELECT COUNT(*) FROM companies WHERE client_of = 1000' - has_many :clients_using_finder_sql, :class_name => "Client", :finder_sql => 'SELECT * FROM companies WHERE 1=1' + has_many :dependent_clients_of_firm, -> { order "id" }, :foreign_key => "client_of", :class_name => "Client", :dependent => :destroy + has_many :exclusively_dependent_clients_of_firm, -> { order "id" }, :foreign_key => "client_of", :class_name => "Client", :dependent => :delete_all + has_many :limited_clients, -> { limit 1 }, :class_name => "Client" + has_many :clients_with_interpolated_conditions, ->(firm) { where "rating > #{firm.rating}" }, :class_name => "Client" + has_many :clients_like_ms, -> { where("name = 'Microsoft'").order("id") }, :class_name => "Client" + has_many :clients_like_ms_with_hash_conditions, -> { where(:name => 'Microsoft').order("id") }, :class_name => "Client" + ActiveSupport::Deprecation.silence do + has_many :clients_using_sql, :class_name => "Client", :finder_sql => proc { "SELECT * FROM companies WHERE client_of = #{id}" } + has_many :clients_using_counter_sql, :class_name => "Client", + :finder_sql => proc { "SELECT * FROM companies WHERE client_of = #{id} " }, + :counter_sql => proc { "SELECT COUNT(*) FROM companies WHERE client_of = #{id}" } + has_many :clients_using_zero_counter_sql, :class_name => "Client", + :finder_sql => proc { "SELECT * FROM companies WHERE client_of = #{id}" }, + :counter_sql => proc { "SELECT 0 FROM companies WHERE client_of = #{id}" } + has_many :no_clients_using_counter_sql, :class_name => "Client", + :finder_sql => 'SELECT * FROM companies WHERE client_of = 1000', + :counter_sql => 'SELECT COUNT(*) FROM companies WHERE client_of = 1000' + has_many :clients_using_finder_sql, :class_name => "Client", :finder_sql => 'SELECT * FROM companies WHERE 1=1' + end has_many :plain_clients, :class_name => 'Client' - has_many :readonly_clients, :class_name => 'Client', :readonly => true + has_many :readonly_clients, -> { readonly }, :class_name => 'Client' has_many :clients_using_primary_key, :class_name => 'Client', :primary_key => 'name', :foreign_key => 'firm_name' has_many :clients_using_primary_key_with_delete_all, :class_name => 'Client', :primary_key => 'name', :foreign_key => 'firm_name', :dependent => :delete_all - has_many :clients_grouped_by_firm_id, :class_name => "Client", :group => "firm_id", :select => "firm_id" - has_many :clients_grouped_by_name, :class_name => "Client", :group => "name", :select => "name" + has_many :clients_grouped_by_firm_id, -> { group("firm_id").select("firm_id") }, :class_name => "Client" + has_many :clients_grouped_by_name, -> { group("name").select("name") }, :class_name => "Client" has_one :account, :foreign_key => "firm_id", :dependent => :destroy, :validate => true has_one :unvalidated_account, :foreign_key => "firm_id", :class_name => 'Account', :validate => false - has_one :account_with_select, :foreign_key => "firm_id", :select => "id, firm_id", :class_name=>'Account' - has_one :readonly_account, :foreign_key => "firm_id", :class_name => "Account", :readonly => true + has_one :account_with_select, -> { select("id, firm_id") }, :foreign_key => "firm_id", :class_name=>'Account' + has_one :readonly_account, -> { readonly }, :foreign_key => "firm_id", :class_name => "Account" # added order by id as in fixtures there are two accounts for Rails Core # Oracle tests were failing because of that as the second fixture was selected - has_one :account_using_primary_key, :primary_key => "firm_id", :class_name => "Account", :order => "id" + has_one :account_using_primary_key, -> { order('id') }, :primary_key => "firm_id", :class_name => "Account" has_one :account_using_foreign_and_primary_keys, :foreign_key => "firm_name", :primary_key => "name", :class_name => "Account" has_one :deletable_account, :foreign_key => "firm_id", :class_name => "Account", :dependent => :delete - has_one :account_limit_500_with_hash_conditions, :foreign_key => "firm_id", :class_name => "Account", :conditions => { :credit_limit => 500 } + has_one :account_limit_500_with_hash_conditions, -> { where :credit_limit => 500 }, :foreign_key => "firm_id", :class_name => "Account" has_one :unautosaved_account, :foreign_key => "firm_id", :class_name => 'Account', :autosave => false has_many :accounts has_many :unautosaved_accounts, :foreign_key => "firm_id", :class_name => 'Account', :autosave => false - has_many :association_with_references, :class_name => 'Client', :references => :foo + has_many :association_with_references, -> { references(:foo) }, :class_name => 'Client' def log @log ||= [] @@ -111,20 +115,32 @@ class DependentFirm < Company end class RestrictedFirm < Company - has_one :account, :foreign_key => "firm_id", :dependent => :restrict, :order => "id" - has_many :companies, :foreign_key => 'client_of', :order => "id", :dependent => :restrict + ActiveSupport::Deprecation.silence do + has_one :account, -> { order("id") }, :foreign_key => "firm_id", :dependent => :restrict + has_many :companies, -> { order("id") }, :foreign_key => 'client_of', :dependent => :restrict + end +end + +class RestrictedWithExceptionFirm < Company + has_one :account, -> { order("id") }, :foreign_key => "firm_id", :dependent => :restrict_with_exception + has_many :companies, -> { order("id") }, :foreign_key => 'client_of', :dependent => :restrict_with_exception +end + +class RestrictedWithErrorFirm < Company + has_one :account, -> { order("id") }, :foreign_key => "firm_id", :dependent => :restrict_with_error + has_many :companies, -> { order("id") }, :foreign_key => 'client_of', :dependent => :restrict_with_error end class Client < Company belongs_to :firm, :foreign_key => "client_of" belongs_to :firm_with_basic_id, :class_name => "Firm", :foreign_key => "firm_id" - belongs_to :firm_with_select, :class_name => "Firm", :foreign_key => "firm_id", :select => "id" + belongs_to :firm_with_select, -> { select("id") }, :class_name => "Firm", :foreign_key => "firm_id" belongs_to :firm_with_other_name, :class_name => "Firm", :foreign_key => "client_of" - belongs_to :firm_with_condition, :class_name => "Firm", :foreign_key => "client_of", :conditions => ["1 = ?", 1] + belongs_to :firm_with_condition, -> { where "1 = ?", 1 }, :class_name => "Firm", :foreign_key => "client_of" belongs_to :firm_with_primary_key, :class_name => "Firm", :primary_key => "name", :foreign_key => "firm_name" belongs_to :firm_with_primary_key_symbols, :class_name => "Firm", :primary_key => :name, :foreign_key => :firm_name - belongs_to :readonly_firm, :class_name => "Firm", :foreign_key => "firm_id", :readonly => true - belongs_to :bob_firm, :class_name => "Firm", :foreign_key => "client_of", :conditions => { :name => "Bob" } + belongs_to :readonly_firm, -> { readonly }, :class_name => "Firm", :foreign_key => "firm_id" + belongs_to :bob_firm, -> { where :name => "Bob" }, :class_name => "Firm", :foreign_key => "client_of" has_many :accounts, :through => :firm belongs_to :account @@ -179,9 +195,9 @@ end class ExclusivelyDependentFirm < Company has_one :account, :foreign_key => "firm_id", :dependent => :delete - has_many :dependent_sanitized_conditional_clients_of_firm, :foreign_key => "client_of", :class_name => "Client", :order => "id", :dependent => :delete_all, :conditions => "name = 'BigShot Inc.'" - has_many :dependent_conditional_clients_of_firm, :foreign_key => "client_of", :class_name => "Client", :order => "id", :dependent => :delete_all, :conditions => ["name = ?", 'BigShot Inc.'] - has_many :dependent_hash_conditional_clients_of_firm, :foreign_key => "client_of", :class_name => "Client", :order => "id", :dependent => :delete_all, :conditions => {:name => 'BigShot Inc.'} + has_many :dependent_sanitized_conditional_clients_of_firm, -> { order("id").where("name = 'BigShot Inc.'") }, :foreign_key => "client_of", :class_name => "Client", :dependent => :delete_all + has_many :dependent_conditional_clients_of_firm, -> { order("id").where("name = ?", 'BigShot Inc.') }, :foreign_key => "client_of", :class_name => "Client", :dependent => :delete_all + has_many :dependent_hash_conditional_clients_of_firm, -> { order("id").where(:name => 'BigShot Inc.') }, :foreign_key => "client_of", :class_name => "Client", :dependent => :delete_all end class SpecialClient < Client diff --git a/activerecord/test/models/company_in_module.rb b/activerecord/test/models/company_in_module.rb index 2c8c30efb4..eb2aedc425 100644 --- a/activerecord/test/models/company_in_module.rb +++ b/activerecord/test/models/company_in_module.rb @@ -7,11 +7,13 @@ module MyApplication end class Firm < Company - has_many :clients, :order => "id", :dependent => :destroy - has_many :clients_sorted_desc, :class_name => "Client", :order => "id DESC" - has_many :clients_of_firm, :foreign_key => "client_of", :class_name => "Client", :order => "id" - has_many :clients_like_ms, :conditions => "name = 'Microsoft'", :class_name => "Client", :order => "id" - has_many :clients_using_sql, :class_name => "Client", :finder_sql => 'SELECT * FROM companies WHERE client_of = #{id}' + has_many :clients, -> { order("id") }, :dependent => :destroy + has_many :clients_sorted_desc, -> { order("id DESC") }, :class_name => "Client" + has_many :clients_of_firm, -> { order "id" }, :foreign_key => "client_of", :class_name => "Client" + has_many :clients_like_ms, -> { where("name = 'Microsoft'").order("id") }, :class_name => "Client" + ActiveSupport::Deprecation.silence do + has_many :clients_using_sql, :class_name => "Client", :finder_sql => 'SELECT * FROM companies WHERE client_of = #{id}' + end has_one :account, :class_name => 'MyApplication::Billing::Account', :dependent => :destroy end diff --git a/activerecord/test/models/contact.rb b/activerecord/test/models/contact.rb index 3d15c7fbed..a1cb8d62b6 100644 --- a/activerecord/test/models/contact.rb +++ b/activerecord/test/models/contact.rb @@ -1,25 +1,40 @@ -class Contact < ActiveRecord::Base - establish_connection(:adapter => 'fake') +module ContactFakeColumns + def self.extended(base) + base.class_eval do + establish_connection(:adapter => 'fake') + + connection.tables = [table_name] + connection.primary_keys = { + table_name => 'id' + } + + column :name, :string + column :age, :integer + column :avatar, :binary + column :created_at, :datetime + column :awesome, :boolean + column :preferences, :string + column :alternative_id, :integer + + serialize :preferences - connection.tables = ['contacts'] - connection.primary_keys = { - 'contacts' => 'id' - } + belongs_to :alternative, :class_name => 'Contact' + end + end # mock out self.columns so no pesky db is needed for these tests - def self.column(name, sql_type = nil, options = {}) - connection.merge_column('contacts', name, sql_type, options) + def column(name, sql_type = nil, options = {}) + connection.merge_column(table_name, name, sql_type, options) end +end - column :name, :string - column :age, :integer - column :avatar, :binary - column :created_at, :datetime - column :awesome, :boolean - column :preferences, :string - column :alternative_id, :integer +class Contact < ActiveRecord::Base + extend ContactFakeColumns +end - serialize :preferences +class ContactSti < ActiveRecord::Base + extend ContactFakeColumns + column :type, :string - belongs_to :alternative, :class_name => 'Contact' + def type; 'ContactSti' end end diff --git a/activerecord/test/models/developer.rb b/activerecord/test/models/developer.rb index 83482f4d07..622dd75aeb 100644 --- a/activerecord/test/models/developer.rb +++ b/activerecord/test/models/developer.rb @@ -2,42 +2,42 @@ require 'ostruct' module DeveloperProjectsAssociationExtension def find_most_recent - scoped(:order => "id DESC").first + order("id DESC").first end end module DeveloperProjectsAssociationExtension2 def find_least_recent - scoped(:order => "id ASC").first + order("id ASC").first end end class Developer < ActiveRecord::Base has_and_belongs_to_many :projects do def find_most_recent - scoped(:order => "id DESC").first + order("id DESC").first end end has_and_belongs_to_many :projects_extended_by_name, + -> { extending(DeveloperProjectsAssociationExtension) }, :class_name => "Project", :join_table => "developers_projects", - :association_foreign_key => "project_id", - :extend => DeveloperProjectsAssociationExtension + :association_foreign_key => "project_id" has_and_belongs_to_many :projects_extended_by_name_twice, + -> { extending(DeveloperProjectsAssociationExtension, DeveloperProjectsAssociationExtension2) }, :class_name => "Project", :join_table => "developers_projects", - :association_foreign_key => "project_id", - :extend => [DeveloperProjectsAssociationExtension, DeveloperProjectsAssociationExtension2] + :association_foreign_key => "project_id" has_and_belongs_to_many :projects_extended_by_name_and_block, + -> { extending(DeveloperProjectsAssociationExtension) }, :class_name => "Project", :join_table => "developers_projects", - :association_foreign_key => "project_id", - :extend => DeveloperProjectsAssociationExtension do + :association_foreign_key => "project_id" do def find_least_recent - scoped(:order => "id ASC").first + order("id ASC").first end end @@ -181,14 +181,14 @@ 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' + has_and_belongs_to_many :projects, -> { order('projects.id') }, :foreign_key => 'developer_id', :join_table => 'developers_projects' 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' + has_and_belongs_to_many :projects, -> { order('projects.id') }, :foreign_key => 'developer_id', :join_table => 'developers_projects' def self.default_scope includes(:projects) @@ -197,21 +197,21 @@ 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' + has_and_belongs_to_many :projects, -> { order('projects.id') }, :foreign_key => 'developer_id', :join_table => 'developers_projects' 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' + has_and_belongs_to_many :projects, -> { order('projects.id') }, :foreign_key => 'developer_id', :join_table => 'developers_projects' 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' + has_and_belongs_to_many :projects, -> { order('projects.id') }, :foreign_key => 'developer_id', :join_table => 'developers_projects' default_scope OpenStruct.new(:call => includes(:projects)) end diff --git a/activerecord/test/models/friendship.rb b/activerecord/test/models/friendship.rb new file mode 100644 index 0000000000..6b4f7acc38 --- /dev/null +++ b/activerecord/test/models/friendship.rb @@ -0,0 +1,4 @@ +class Friendship < ActiveRecord::Base + belongs_to :friend, class_name: 'Person' + belongs_to :follower, foreign_key: 'friend_id', class_name: 'Person', counter_cache: :followers_count +end diff --git a/activerecord/test/models/liquid.rb b/activerecord/test/models/liquid.rb index 3fcd5e4b69..6cfd443e75 100644 --- a/activerecord/test/models/liquid.rb +++ b/activerecord/test/models/liquid.rb @@ -1,5 +1,5 @@ class Liquid < ActiveRecord::Base self.table_name = :liquid - has_many :molecules, :uniq => true + has_many :molecules, -> { uniq } end diff --git a/activerecord/test/models/member.rb b/activerecord/test/models/member.rb index 11a0f4ff63..1134b09d8b 100644 --- a/activerecord/test/models/member.rb +++ b/activerecord/test/models/member.rb @@ -5,8 +5,8 @@ class Member < ActiveRecord::Base has_many :fellow_members, :through => :club, :source => :members has_one :club, :through => :current_membership has_one :selected_club, :through => :selected_membership, :source => :club - has_one :favourite_club, :through => :membership, :conditions => ["memberships.favourite = ?", true], :source => :club - has_one :hairy_club, :through => :membership, :conditions => {:clubs => {:name => "Moustache and Eyebrow Fancier Club"}}, :source => :club + has_one :favourite_club, -> { where "memberships.favourite = ?", true }, :through => :membership, :source => :club + has_one :hairy_club, -> { where :clubs => {:name => "Moustache and Eyebrow Fancier Club"} }, :through => :membership, :source => :club has_one :sponsor, :as => :sponsorable has_one :sponsor_club, :through => :sponsor has_one :member_detail @@ -24,9 +24,13 @@ class Member < ActiveRecord::Base has_one :club_category, :through => :club, :source => :category - has_many :current_memberships + has_many :current_memberships, -> { where :favourite => true } + has_many :clubs, :through => :current_memberships + has_one :club_through_many, :through => :current_memberships, :source => :club +end - has_many :current_memberships, :conditions => { :favourite => true } - has_many :clubs, :through => :current_memberships +class SelfMember < ActiveRecord::Base + self.table_name = "members" + has_and_belongs_to_many :friends, :class_name => "SelfMember", :join_table => "member_friends" end diff --git a/activerecord/test/models/person.rb b/activerecord/test/models/person.rb index d5c0b351aa..6e6ff29f77 100644 --- a/activerecord/test/models/person.rb +++ b/activerecord/test/models/person.rb @@ -5,14 +5,16 @@ class Person < ActiveRecord::Base has_many :posts, :through => :readers has_many :secure_posts, :through => :secure_readers - has_many :posts_with_no_comments, :through => :readers, :source => :post, :include => :comments, - :conditions => 'comments.id is null', :references => :comments + has_many :posts_with_no_comments, -> { includes(:comments).where('comments.id is null').references(:comments) }, + :through => :readers, :source => :post + + has_many :followers, foreign_key: 'friend_id', class_name: 'Friendship' has_many :references has_many :bad_references - has_many :fixed_bad_references, :conditions => { :favourite => true }, :class_name => 'BadReference' - has_one :favourite_reference, :class_name => 'Reference', :conditions => ['favourite=?', true] - has_many :posts_with_comments_sorted_by_comment_id, :through => :readers, :source => :post, :include => :comments, :order => 'comments.id' + has_many :fixed_bad_references, -> { where :favourite => true }, :class_name => 'BadReference' + has_one :favourite_reference, -> { where 'favourite=?', true }, :class_name => 'Reference' + has_many :posts_with_comments_sorted_by_comment_id, -> { includes(:comments).order('comments.id') }, :through => :readers, :source => :post has_many :jobs, :through => :references has_many :jobs_with_dependent_destroy, :source => :job, :through => :references, :dependent => :destroy @@ -88,6 +90,25 @@ class TightDescendant < TightPerson; end class RichPerson < ActiveRecord::Base self.table_name = 'people' - + has_and_belongs_to_many :treasures, :join_table => 'peoples_treasures' end + +class NestedPerson < ActiveRecord::Base + self.table_name = 'people' + + attr_accessible :first_name, :best_friend_first_name, :best_friend_attributes + attr_accessible :first_name, :gender, :comments, :as => :admin + attr_accessible :best_friend_attributes, :best_friend_first_name, :as => :admin + + has_one :best_friend, :class_name => 'NestedPerson', :foreign_key => :best_friend_id + accepts_nested_attributes_for :best_friend, :update_only => true + + def comments=(new_comments) + raise RuntimeError + end + + def best_friend_first_name=(new_name) + assign_attributes({ :best_friend_attributes => { :first_name => new_name } }) + end +end diff --git a/activerecord/test/models/pirate.rb b/activerecord/test/models/pirate.rb index 5e0f5323e6..609b9369a9 100644 --- a/activerecord/test/models/pirate.rb +++ b/activerecord/test/models/pirate.rb @@ -1,7 +1,7 @@ class Pirate < ActiveRecord::Base belongs_to :parrot, :validate => true belongs_to :non_validated_parrot, :class_name => 'Parrot' - has_and_belongs_to_many :parrots, :validate => true, :order => 'parrots.id ASC' + has_and_belongs_to_many :parrots, -> { order('parrots.id ASC') }, :validate => true has_and_belongs_to_many :non_validated_parrots, :class_name => 'Parrot' has_and_belongs_to_many :parrots_with_method_callbacks, :class_name => "Parrot", :before_add => :log_before_add, @@ -21,7 +21,7 @@ class Pirate < ActiveRecord::Base has_one :ship has_one :update_only_ship, :class_name => 'Ship' has_one :non_validated_ship, :class_name => 'Ship' - has_many :birds, :order => 'birds.id ASC' + has_many :birds, -> { order('birds.id ASC') } has_many :birds_with_method_callbacks, :class_name => "Bird", :before_add => :log_before_add, :after_add => :log_after_add, @@ -34,7 +34,7 @@ class Pirate < ActiveRecord::Base :after_remove => proc {|p,b| p.ship_log << "after_removing_proc_bird_#{b.id}"} has_many :birds_with_reject_all_blank, :class_name => "Bird" - has_one :foo_bulb, :foreign_key => :car_id, :class_name => "Bulb", :conditions => { :name => 'foo' } + has_one :foo_bulb, -> { where :name => 'foo' }, :foreign_key => :car_id, :class_name => "Bulb" accepts_nested_attributes_for :parrots, :birds, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? } accepts_nested_attributes_for :ship, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? } diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index 1aaf9a1b82..9c5b7310ff 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -16,14 +16,14 @@ class Post < ActiveRecord::Base end end - belongs_to :author_with_posts, :class_name => "Author", :foreign_key => :author_id, :include => :posts - belongs_to :author_with_address, :class_name => "Author", :foreign_key => :author_id, :include => :author_address + belongs_to :author_with_posts, -> { includes(:posts) }, :class_name => "Author", :foreign_key => :author_id + belongs_to :author_with_address, -> { includes(:author_address) }, :class_name => "Author", :foreign_key => :author_id def first_comment super.body end - has_one :first_comment, :class_name => 'Comment', :order => 'id ASC' - has_one :last_comment, :class_name => 'Comment', :order => 'id desc' + has_one :first_comment, -> { order('id ASC') }, :class_name => 'Comment' + has_one :last_comment, -> { order('id desc') }, :class_name => 'Comment' scope :with_special_comments, -> { joins(:comments).where(:comments => {:type => 'SpecialComment'}) } scope :with_very_special_comments, -> { joins(:comments).where(:comments => {:type => 'VerySpecialComment'}) } @@ -31,7 +31,7 @@ class Post < ActiveRecord::Base has_many :comments do def find_most_recent - scoped(:order => "id DESC").first + order("id DESC").first end def newest @@ -47,13 +47,14 @@ class Post < ActiveRecord::Base has_many :author_categorizations, :through => :author, :source => :categorizations has_many :author_addresses, :through => :author - has_many :comments_with_interpolated_conditions, :class_name => 'Comment', - :conditions => proc { ["#{"#{aliased_table_name}." rescue ""}body = ?", 'Thank you for the welcome'] } + has_many :comments_with_interpolated_conditions, + ->(p) { where "#{"#{p.aliased_table_name}." rescue ""}body = ?", 'Thank you for the welcome' }, + :class_name => 'Comment' has_one :very_special_comment - has_one :very_special_comment_with_post, :class_name => "VerySpecialComment", :include => :post + has_one :very_special_comment_with_post, -> { includes(:post) }, :class_name => "VerySpecialComment" has_many :special_comments - has_many :nonexistant_comments, :class_name => 'Comment', :conditions => 'comments.id < 0' + has_many :nonexistant_comments, -> { where 'comments.id < 0' }, :class_name => 'Comment' has_many :special_comments_ratings, :through => :special_comments, :source => :ratings has_many :special_comments_ratings_taggings, :through => :special_comments_ratings, :source => :taggings @@ -64,33 +65,30 @@ class Post < ActiveRecord::Base has_many :taggings, :as => :taggable has_many :tags, :through => :taggings do def add_joins_and_select - scoped(:select => 'tags.*, authors.id as author_id', - :joins => 'left outer join posts on taggings.taggable_id = posts.id left outer join authors on posts.author_id = authors.id').all + select('tags.*, authors.id as author_id') + .joins('left outer join posts on taggings.taggable_id = posts.id left outer join authors on posts.author_id = authors.id') + .to_a end end - has_many :interpolated_taggings, :class_name => 'Tagging', :as => :taggable, :conditions => proc { "1 = #{1}" } - has_many :interpolated_tags, :through => :taggings - has_many :interpolated_tags_2, :through => :interpolated_taggings, :source => :tag - has_many :taggings_with_delete_all, :class_name => 'Tagging', :as => :taggable, :dependent => :delete_all has_many :taggings_with_destroy, :class_name => 'Tagging', :as => :taggable, :dependent => :destroy has_many :tags_with_destroy, :through => :taggings, :source => :tag, :dependent => :destroy has_many :tags_with_nullify, :through => :taggings, :source => :tag, :dependent => :nullify - has_many :misc_tags, :through => :taggings, :source => :tag, :conditions => { :tags => { :name => 'Misc' } } + has_many :misc_tags, -> { where :tags => { :name => 'Misc' } }, :through => :taggings, :source => :tag has_many :funky_tags, :through => :taggings, :source => :tag has_many :super_tags, :through => :taggings has_many :tags_with_primary_key, :through => :taggings, :source => :tag_with_primary_key has_one :tagging, :as => :taggable - has_many :first_taggings, :as => :taggable, :class_name => 'Tagging', :conditions => { :taggings => { :comment => 'first' } } - has_many :first_blue_tags, :through => :first_taggings, :source => :tag, :conditions => { :tags => { :name => 'Blue' } } + has_many :first_taggings, -> { where :taggings => { :comment => 'first' } }, :as => :taggable, :class_name => 'Tagging' + has_many :first_blue_tags, -> { where :tags => { :name => 'Blue' } }, :through => :first_taggings, :source => :tag - has_many :first_blue_tags_2, :through => :taggings, :source => :blue_tag, :conditions => { :taggings => { :comment => 'first' } } + has_many :first_blue_tags_2, -> { where :taggings => { :comment => 'first' } }, :through => :taggings, :source => :blue_tag - has_many :invalid_taggings, :as => :taggable, :class_name => "Tagging", :conditions => 'taggings.id < 0' + has_many :invalid_taggings, -> { where 'taggings.id < 0' }, :as => :taggable, :class_name => "Tagging" has_many :invalid_tags, :through => :invalid_taggings, :source => :tag has_many :categorizations, :foreign_key => :category_id @@ -109,7 +107,7 @@ class Post < ActiveRecord::Base has_many :readers has_many :secure_readers - has_many :readers_with_person, :include => :person, :class_name => "Reader" + has_many :readers_with_person, -> { includes(:person) }, :class_name => "Reader" has_many :people, :through => :readers has_many :secure_people, :through => :secure_readers has_many :single_people, :through => :readers @@ -118,7 +116,7 @@ class Post < ActiveRecord::Base :after_add => lambda {|owner, reader| log(:added, :after, reader.first_name) }, :before_remove => lambda {|owner, reader| log(:removed, :before, reader.first_name) }, :after_remove => lambda {|owner, reader| log(:removed, :after, reader.first_name) } - has_many :skimmers, :class_name => 'Reader', :conditions => { :skimmer => true } + has_many :skimmers, -> { where :skimmer => true }, :class_name => 'Reader' has_many :impatient_people, :through => :skimmers, :source => :person def self.top(limit) diff --git a/activerecord/test/models/project.rb b/activerecord/test/models/project.rb index 32ce164995..af3ec4be83 100644 --- a/activerecord/test/models/project.rb +++ b/activerecord/test/models/project.rb @@ -1,26 +1,30 @@ class Project < ActiveRecord::Base - has_and_belongs_to_many :developers, :uniq => true, :order => 'developers.name desc, developers.id desc' - has_and_belongs_to_many :readonly_developers, :class_name => "Developer", :readonly => true - has_and_belongs_to_many :selected_developers, :class_name => "Developer", :select => "developers.*", :uniq => true - has_and_belongs_to_many :non_unique_developers, :order => 'developers.name desc, developers.id desc', :class_name => 'Developer' - has_and_belongs_to_many :limited_developers, :class_name => "Developer", :limit => 1 - has_and_belongs_to_many :developers_named_david, :class_name => "Developer", :conditions => "name = 'David'", :uniq => true - has_and_belongs_to_many :developers_named_david_with_hash_conditions, :class_name => "Developer", :conditions => { :name => 'David' }, :uniq => true - has_and_belongs_to_many :salaried_developers, :class_name => "Developer", :conditions => "salary > 0" - has_and_belongs_to_many :developers_with_finder_sql, :class_name => "Developer", :finder_sql => proc { "SELECT t.*, j.* FROM developers_projects j, developers t WHERE t.id = j.developer_id AND j.project_id = #{id} ORDER BY t.id" } - has_and_belongs_to_many :developers_with_multiline_finder_sql, :class_name => "Developer", :finder_sql => proc { - "SELECT - t.*, j.* - FROM - developers_projects j, - developers t WHERE t.id = j.developer_id AND j.project_id = #{id} ORDER BY t.id" - } - has_and_belongs_to_many :developers_by_sql, :class_name => "Developer", :delete_sql => proc { |record| "DELETE FROM developers_projects WHERE project_id = #{id} AND developer_id = #{record.id}" } + has_and_belongs_to_many :developers, -> { uniq.order 'developers.name desc, developers.id desc' } + has_and_belongs_to_many :readonly_developers, -> { readonly }, :class_name => "Developer" + has_and_belongs_to_many :selected_developers, -> { uniq.select "developers.*" }, :class_name => "Developer" + has_and_belongs_to_many :non_unique_developers, -> { order 'developers.name desc, developers.id desc' }, :class_name => 'Developer' + has_and_belongs_to_many :limited_developers, -> { limit 1 }, :class_name => "Developer" + has_and_belongs_to_many :developers_named_david, -> { where("name = 'David'").uniq }, :class_name => "Developer" + has_and_belongs_to_many :developers_named_david_with_hash_conditions, -> { where(:name => 'David').uniq }, :class_name => "Developer" + has_and_belongs_to_many :salaried_developers, -> { where "salary > 0" }, :class_name => "Developer" + + ActiveSupport::Deprecation.silence do + has_and_belongs_to_many :developers_with_finder_sql, :class_name => "Developer", :finder_sql => proc { "SELECT t.*, j.* FROM developers_projects j, developers t WHERE t.id = j.developer_id AND j.project_id = #{id} ORDER BY t.id" } + has_and_belongs_to_many :developers_with_multiline_finder_sql, :class_name => "Developer", :finder_sql => proc { + "SELECT + t.*, j.* + FROM + developers_projects j, + developers t WHERE t.id = j.developer_id AND j.project_id = #{id} ORDER BY t.id" + } + has_and_belongs_to_many :developers_by_sql, :class_name => "Developer", :delete_sql => proc { |record| "DELETE FROM developers_projects WHERE project_id = #{id} AND developer_id = #{record.id}" } + end + has_and_belongs_to_many :developers_with_callbacks, :class_name => "Developer", :before_add => Proc.new {|o, r| o.developers_log << "before_adding#{r.id || '<new>'}"}, :after_add => Proc.new {|o, r| o.developers_log << "after_adding#{r.id || '<new>'}"}, :before_remove => Proc.new {|o, r| o.developers_log << "before_removing#{r.id}"}, :after_remove => Proc.new {|o, r| o.developers_log << "after_removing#{r.id}"} - has_and_belongs_to_many :well_payed_salary_groups, :class_name => "Developer", :group => "developers.salary", :having => "SUM(salary) > 10000", :select => "SUM(salary) as salary" + has_and_belongs_to_many :well_payed_salary_groups, -> { group("developers.salary").having("SUM(salary) > 10000").select("SUM(salary) as salary") }, :class_name => "Developer" attr_accessor :developers_log after_initialize :set_developers_log @@ -32,7 +36,7 @@ class Project < ActiveRecord::Base def self.all_as_method all end - scope :all_as_scope, -> { scoped } + scope :all_as_scope, -> { all } end class SpecialProject < Project diff --git a/activerecord/test/models/sponsor.rb b/activerecord/test/models/sponsor.rb index aa4a3638fd..ec3dcf8a97 100644 --- a/activerecord/test/models/sponsor.rb +++ b/activerecord/test/models/sponsor.rb @@ -2,6 +2,6 @@ class Sponsor < ActiveRecord::Base belongs_to :sponsor_club, :class_name => "Club", :foreign_key => "club_id" belongs_to :sponsorable, :polymorphic => true belongs_to :thing, :polymorphic => true, :foreign_type => :sponsorable_type, :foreign_key => :sponsorable_id - belongs_to :sponsorable_with_conditions, :polymorphic => true, - :foreign_type => 'sponsorable_type', :foreign_key => 'sponsorable_id', :conditions => {:name => 'Ernie'} + belongs_to :sponsorable_with_conditions, -> { where :name => 'Ernie'}, :polymorphic => true, + :foreign_type => 'sponsorable_type', :foreign_key => 'sponsorable_id' end diff --git a/activerecord/test/models/tagging.rb b/activerecord/test/models/tagging.rb index ef323df158..f91f2ad2e9 100644 --- a/activerecord/test/models/tagging.rb +++ b/activerecord/test/models/tagging.rb @@ -3,12 +3,11 @@ module Taggable end class Tagging < ActiveRecord::Base - belongs_to :tag, :include => :tagging + belongs_to :tag, -> { includes(:tagging) } belongs_to :super_tag, :class_name => 'Tag', :foreign_key => 'super_tag_id' belongs_to :invalid_tag, :class_name => 'Tag', :foreign_key => 'tag_id' - belongs_to :blue_tag, :class_name => 'Tag', :foreign_key => :tag_id, :conditions => { :tags => { :name => 'Blue' } } + belongs_to :blue_tag, -> { where :tags => { :name => 'Blue' } }, :class_name => 'Tag', :foreign_key => :tag_id belongs_to :tag_with_primary_key, :class_name => 'Tag', :foreign_key => :tag_id, :primary_key => :custom_primary_key - belongs_to :interpolated_tag, :class_name => 'Tag', :foreign_key => :tag_id, :conditions => proc { "1 = #{1}" } belongs_to :taggable, :polymorphic => true, :counter_cache => true has_many :things, :through => :taggable end diff --git a/activerecord/test/models/topic.rb b/activerecord/test/models/topic.rb index 079e403444..4b27c16681 100644 --- a/activerecord/test/models/topic.rb +++ b/activerecord/test/models/topic.rb @@ -1,5 +1,5 @@ class Topic < ActiveRecord::Base - scope :base, -> { scoped } + scope :base, -> { all } scope :written_before, lambda { |time| if time where 'written_on < ?', time @@ -8,13 +8,13 @@ class Topic < ActiveRecord::Base scope :approved, -> { where(:approved => true) } scope :rejected, -> { where(:approved => false) } - scope :scope_with_lambda, lambda { scoped } + scope :scope_with_lambda, lambda { all } scope :by_lifo, -> { where(:author_name => 'lifo') } scope :replied, -> { where 'replies_count > 0' } scope 'approved_as_string', -> { where(:approved => true) } - scope :anonymous_extension, -> { scoped } do + scope :anonymous_extension, -> { all } do def one 1 end @@ -58,6 +58,8 @@ class Topic < ActiveRecord::Base id end + alias_attribute :heading, :title + before_validation :before_validation_for_transaction before_save :before_save_for_transaction before_destroy :before_destroy_for_transaction diff --git a/activerecord/test/schema/mysql2_specific_schema.rb b/activerecord/test/schema/mysql2_specific_schema.rb index 65b6f9f227..24a43d7ece 100644 --- a/activerecord/test/schema/mysql2_specific_schema.rb +++ b/activerecord/test/schema/mysql2_specific_schema.rb @@ -32,4 +32,13 @@ CREATE TABLE collation_tests ( ) CHARACTER SET utf8 COLLATE utf8_general_ci SQL + ActiveRecord::Base.connection.execute <<-SQL +DROP TABLE IF EXISTS enum_tests; +SQL + + ActiveRecord::Base.connection.execute <<-SQL +CREATE TABLE enum_tests ( + enum_column ENUM('true','false') +) +SQL end diff --git a/activerecord/test/schema/mysql_specific_schema.rb b/activerecord/test/schema/mysql_specific_schema.rb index 7d324f98c4..802c08b819 100644 --- a/activerecord/test/schema/mysql_specific_schema.rb +++ b/activerecord/test/schema/mysql_specific_schema.rb @@ -43,4 +43,14 @@ CREATE TABLE collation_tests ( ) CHARACTER SET utf8 COLLATE utf8_general_ci SQL + ActiveRecord::Base.connection.execute <<-SQL +DROP TABLE IF EXISTS enum_tests; +SQL + + ActiveRecord::Base.connection.execute <<-SQL +CREATE TABLE enum_tests ( + enum_column ENUM('true','false') +) +SQL + end diff --git a/activerecord/test/schema/postgresql_specific_schema.rb b/activerecord/test/schema/postgresql_specific_schema.rb index e51db50ae3..5f01f1fc50 100644 --- a/activerecord/test/schema/postgresql_specific_schema.rb +++ b/activerecord/test/schema/postgresql_specific_schema.rb @@ -1,6 +1,6 @@ ActiveRecord::Schema.define do - %w(postgresql_tsvectors postgresql_hstores postgresql_arrays postgresql_moneys postgresql_numbers postgresql_times postgresql_network_addresses postgresql_bit_strings + %w(postgresql_tsvectors postgresql_hstores postgresql_arrays postgresql_moneys postgresql_numbers postgresql_times postgresql_network_addresses postgresql_bit_strings postgresql_uuids postgresql_oids postgresql_xml_data_type defaults geometrics postgresql_timestamp_with_zones postgresql_partitioned_table postgresql_partitioned_table_parent).each do |table_name| execute "DROP TABLE IF EXISTS #{quote_table_name table_name}" end @@ -59,6 +59,14 @@ _SQL _SQL execute <<_SQL + CREATE TABLE postgresql_uuids ( + id SERIAL PRIMARY KEY, + guid uuid, + compact_guid uuid + ); +_SQL + + execute <<_SQL CREATE TABLE postgresql_tsvectors ( id SERIAL PRIMARY KEY, text_vector tsvector diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 5bcb9652cd..7c45ca27c0 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -37,12 +37,12 @@ ActiveRecord::Schema.define do create_table :admin_users, :force => true do |t| t.string :name - t.text :settings, :null => true + t.string :settings, :null => true, :limit => 1024 # MySQL does not allow default values for blobs. Fake it out with a # big varchar below. - t.string :preferences, :null => false, :default => '', :limit => 1024 + t.string :preferences, :null => true, :default => '', :limit => 1024 t.string :json_data, :null => true, :limit => 1024 - t.string :json_data_empty, :null => false, :default => "", :limit => 1024 + t.string :json_data_empty, :null => true, :default => "", :limit => 1024 t.references :account end @@ -178,7 +178,7 @@ ActiveRecord::Schema.define do t.integer :client_of t.integer :rating, :default => 1 t.integer :account_id - t.string :description, :null => false, :default => "" + t.string :description, :default => "" end add_index :companies, [:firm_id, :type, :rating, :ruby_type], :name => "company_index" @@ -270,6 +270,11 @@ ActiveRecord::Schema.define do t.string :name end + create_table :friendships, :force => true do |t| + t.integer :friend_id + t.integer :person_id + end + create_table :goofy_string_id, :force => true, :id => false do |t| t.string :id, :null => false t.string :info @@ -361,6 +366,11 @@ ActiveRecord::Schema.define do t.string :extra_data end + create_table :member_friends, :force => true, :id => false do |t| + t.integer :member_id + t.integer :friend_id + end + create_table :memberships, :force => true do |t| t.datetime :joined_on t.integer :club_id, :member_id @@ -432,7 +442,6 @@ ActiveRecord::Schema.define do t.string :name t.column :updated_at, :datetime t.column :happy_at, :datetime - t.column :eats_at, :time t.string :essay_id end @@ -472,6 +481,7 @@ ActiveRecord::Schema.define do t.references :number1_fan t.integer :lock_version, :null => false, :default => 0 t.string :comments + t.integer :followers_count, :default => 0 t.references :best_friend t.references :best_friend_of t.timestamps diff --git a/activerecord/test/support/connection.rb b/activerecord/test/support/connection.rb index c176316a05..92736e0ca9 100644 --- a/activerecord/test/support/connection.rb +++ b/activerecord/test/support/connection.rb @@ -1,6 +1,6 @@ require 'active_support/logger' -require_dependency 'models/college' -require_dependency 'models/course' +require 'models/college' +require 'models/course' module ARTest def self.connection_name |