diff options
Diffstat (limited to 'activerecord')
205 files changed, 5070 insertions, 10598 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 36d94fb38b..85880e97ea 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,5 +1,143 @@ ## Rails 4.0.0 (unreleased) ## +* Allow to pass Symbol or Proc into :limit option of #accepts_nested_attributes_for + + *Mikhail Dieterle* + +* ActiveRecord::SessionStore has been extracted from Active Record as `activerecord-session_store` + gem. Please read the `README.md` file on the gem for the usage. *Prem Sichanugrist* + +* 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: @@ -23,11 +161,11 @@ * `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).inspect + # => <ActiveRecord::Relation [#<User ...>, #<User ...>, ...]> - User.where(:age => 30).to_a.inspect - # => [#<User ...>, #<User ...>] + User.where(:age => 30).to_a.inspect + # => [#<User ...>, #<User ...>] The number of records displayed will be limited to 10. @@ -36,18 +174,25 @@ * 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 + 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* @@ -124,29 +269,6 @@ *Joost Baaij & Carlos Antonio da Silva* -* `composed_of` was removed. 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. So, instead of: - - class Person < ActiveRecord::Base - composed_of :address, :mapping => [ %w(address_street street), %w(address_city city) ] - end - - you could write something like this: - - def address - @address ||= Address.new(address_street, address_city) - end - - def address=(address) - self[:address_street] = @address.street - self[:address_city] = @address.city - - @address = address - end - - *Steve Klabnik* - * 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. @@ -155,12 +277,6 @@ * Add uuid datatype support to PostgreSQL adapter. *Konstantin Shabanov* -* `update_attribute` has been removed. 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* - * Added `ActiveRecord::Migration.check_pending!` that raises an error if migrations are pending. *Richard Schneeman* @@ -204,7 +320,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* @@ -220,13 +336,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: @@ -236,7 +351,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 @@ -371,7 +486,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 @@ -388,11 +503,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* @@ -409,24 +524,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 @@ -512,6866 +616,104 @@ * PostgreSQL hstore types are automatically deserialized from the database. +## Rails 3.2.8 (Aug 9, 2012) ## -## Rails 3.2.5 (Jun 1, 2012) ## - -* Restore behavior of Active Record 3.2.3 scopes. - A series of commits relating to preloading and scopes caused a regression. - - *Andrew White* - - -## Rails 3.2.4 (May 31, 2012) ## - -* Perf fix: Don't load the records when doing assoc.delete_all. - GH #6289. *Jon Leighton* - -* Association preloading shouldn't be affected by the current scoping. - This could cause infinite recursion and potentially other problems. - See GH #5667. *Jon Leighton* - -* Datetime attributes are forced to be changed. GH #3965 - -* Fix attribute casting. GH #5549 - -* Fix #5667. Preloading should ignore scoping. - -* Predicate builder should not recurse for determining where columns. - Thanks to Ben Murphy for reporting this! CVE-2012-2661 - - -## Rails 3.2.3 (March 30, 2012) ## - -* Added find_or_create_by_{attribute}! dynamic method. *Andrew White* - -* Whitelist all attribute assignment by default. Change the default for newly generated applications to whitelist all attribute assignment. Also update the generated model classes so users are reminded of the importance of attr_accessible. *NZKoz* - -* Update ActiveRecord::AttributeMethods#attribute_present? to return false for empty strings. *Jacobkg* - -* Fix associations when using per class databases. *larskanis* - -* Revert setting NOT NULL constraints in add_timestamps *fxn* - -* Fix mysql to use proper text types. Fixes #3931. *kennyj* - -* Fix #5069 - Protect foreign key from mass assignment through association builder. *byroot* - - -## Rails 3.2.2 (March 1, 2012) ## - -* No changes. - - -## Rails 3.2.1 (January 26, 2012) ## - -* The threshold for auto EXPLAIN is ignored if there's no logger. *fxn* - -* Call `to_s` on the value passed to `table_name=`, in particular symbols - are supported (regression). *Sergey Nartimov* - -* Fix possible race condition when two threads try to define attribute - methods for the same class. *Jon Leighton* - - -## Rails 3.2.0 (January 20, 2012) ## - -* Added a `with_lock` method to ActiveRecord objects, which starts - a transaction, locks the object (pessimistically) and yields to the block. - The method takes one (optional) parameter and passes it to `lock!`. - - Before: - - class Order < ActiveRecord::Base - def cancel! - transaction do - lock! - # ... cancelling logic - end - end - end - - After: - - class Order < ActiveRecord::Base - def cancel! - with_lock do - # ... cancelling logic - end - end - end - - *Olek Janiszewski* - -* 'on' and 'ON' boolean columns values are type casted to true - *Santiago Pastorino* - -* Added ability to run migrations only for given scope, which allows - to run migrations only from one engine (for example to revert changes - from engine that you want to remove). - - Example: - rake db:migrate SCOPE=blog - - *Piotr Sarnacki* - -* Migrations copied from engines are now scoped with engine's name, - for example 01_create_posts.blog.rb. *Piotr Sarnacki* - -* Implements `AR::Base.silence_auto_explain`. This method allows the user to - selectively disable automatic EXPLAINs within a block. *fxn* - -* Implements automatic EXPLAIN logging for slow queries. - - A new configuration parameter `config.active_record.auto_explain_threshold_in_seconds` - determines what's to be considered a slow query. Setting that to `nil` disables - this feature. Defaults are 0.5 in development mode, and `nil` in test and production - modes. - - As of this writing there's support for SQLite, MySQL (mysql2 adapter), and - PostgreSQL. - - *fxn* - -* Implemented ActiveRecord::Relation#pluck method - - Method returns Array of column value from table under ActiveRecord model - - Client.pluck(:id) - - *Bogdan Gusiev* - -* Automatic closure of connections in threads is deprecated. For example - the following code is deprecated: - - Thread.new { Post.find(1) }.join - - It should be changed to close the database connection at the end of - the thread: - - Thread.new { - Post.find(1) - Post.connection.close - }.join - - Only people who spawn threads in their application code need to worry - about this change. - -* Deprecated: - - * `set_table_name` - * `set_inheritance_column` - * `set_sequence_name` - * `set_primary_key` - * `set_locking_column` - - Use an assignment method instead. For example, instead of `set_table_name`, use `self.table_name=`: - - class Project < ActiveRecord::Base - self.table_name = "project" - end - - Or define your own `self.table_name` method: - - class Post < ActiveRecord::Base - def self.table_name - "special_" + super - end - end - Post.table_name # => "special_posts" - - *Jon Leighton* - -* Generated association methods are created within a separate module to allow overriding and - composition using `super`. For a class named `MyModel`, the module is named - `MyModel::GeneratedFeatureMethods`. It is included into the model class immediately after - the `generated_attributes_methods` module defined in ActiveModel, so association methods - override attribute methods of the same name. *Josh Susser* - -* Implemented ActiveRecord::Relation#explain. *fxn* - -* Add ActiveRecord::Relation#uniq for generating unique queries. - - Before: - - Client.select('DISTINCT name') - - After: - - Client.select(:name).uniq - - This also allows you to revert the uniqueness in a relation: - - Client.select(:name).uniq.uniq(false) - - *Jon Leighton* - -* Support index sort order in sqlite, mysql and postgres adapters. *Vlad Jebelev* - -* Allow the :class_name option for associations to take a symbol (:Client) in addition to - a string ('Client'). - - This is to avoid confusing newbies, and to be consistent with the fact that other options - like :foreign_key already allow a symbol or a string. - - *Jon Leighton* - -* In development mode the db:drop task also drops the test database. For symmetry with - the db:create task. *Dmitriy Kiriyenko* - -* Added ActiveRecord::Base.store for declaring simple single-column key/value stores *DHH* - - class User < ActiveRecord::Base - store :settings, accessors: [ :color, :homepage ] - end - - u = User.new(color: 'black', homepage: '37signals.com') - u.color # Accessor stored attribute - u.settings[:country] = 'Denmark' # Any attribute, even if not specified with an accessor - - -* MySQL: case-insensitive uniqueness validation avoids calling LOWER when - the column already uses a case-insensitive collation. Fixes #561. - - *Joseph Palermo* - -* Transactional fixtures enlist all active database connections. You can test - models on different connections without disabling transactional fixtures. - - *Jeremy Kemper* - -* Add first_or_create, first_or_create!, first_or_initialize methods to Active Record. This is a - better approach over the old find_or_create_by dynamic methods because it's clearer which - arguments are used to find the record and which are used to create it: - - User.where(:first_name => "Scarlett").first_or_create!(:last_name => "Johansson") - - *Andrés Mejía* - -* Fix nested attributes bug where _destroy parameter is taken into account - during :reject_if => :all_blank (fixes #2937) - - *Aaron Christy* - - -## Rails 3.1.4 (March 1, 2012) ## - - * Fix a custom primary key regression *GH 3987* - - *Jon Leighton* - - * Perf fix (second try): don't load records for `has many :dependent => - :delete_all` *GH 3672* - - *Jon Leighton* - - * Fix accessing `proxy_association` method from an association extension - where the calls are chained. *GH #3890* - - (E.g. `post.comments.where(bla).my_proxy_method`) - - *Jon Leighton* - - * Perf fix: MySQL primary key lookup was still slow for very large - tables. *GH 3678* - - *Kenny J* - - * Perf fix: If a table has no primary key, don't repeatedly ask the database for it. - - *Julius de Bruijn* - - -### Rails 3.1.3 (November 20, 2011) ## - -* Perf fix: If we're deleting all records in an association, don't add a IN(..) clause - to the query. *GH 3672* - - *Jon Leighton* - -* Fix bug with referencing other mysql databases in set_table_name. *GH 3690* - -* Fix performance bug with mysql databases on a server with lots of other databses. *GH 3678* - - *Christos Zisopoulos and Kenny J* - - -### Rails 3.1.2 (November 18, 2011) ## - -* Fix bug with PostgreSQLAdapter#indexes. When the search path has multiple schemas, spaces - were not being stripped from the schema names after the first. - - *Sean Kirby* - -* Preserve SELECT columns on the COUNT for finder_sql when possible. *GH 3503* - - *Justin Mazzi* - -* Reset prepared statement cache when schema changes impact statement results. *GH 3335* - - *Aaron Patterson* - -* Postgres: Do not attempt to deallocate a statement if the connection is no longer active. - - *Ian Leitch* - -* Prevent QueryCache leaking database connections. *GH 3243* - - *Mark J. Titorenko* - -* Fix bug where building the conditions of a nested through association could potentially - modify the conditions of the through and/or source association. If you have experienced - bugs with conditions appearing in the wrong queries when using nested through associations, - this probably solves your problems. *GH #3271* - - *Jon Leighton* - -* If a record is removed from a has_many :through, all of the join records relating to that - record should also be removed from the through association's target. - - *Jon Leighton* - -* Fix adding multiple instances of the same record to a has_many :through. *GH #3425* - - *Jon Leighton* - -* Fix creating records in a through association with a polymorphic source type. *GH #3247* - - *Jon Leighton* - -* MySQL: use the information_schema than the describe command when we look for a primary key. *GH #3440* - - *Kenny J* - - -## Rails 3.1.1 (October 7, 2011) ## - -* Add deprecation for the preload_associations method. Fixes #3022. - - *Jon Leighton* - -* Don't require a DB connection when loading a model that uses set_primary_key. GH #2807. - - *Jon Leighton* - -* Fix using select() with a habtm association, e.g. Person.friends.select(:name). GH #3030 and - \#2923. - - *Hendy Tanata* - -* Fix belongs_to polymorphic with custom primary key on target. GH #3104. - - *Jon Leighton* - -* CollectionProxy#replace should change the DB records rather than just mutating the array. - Fixes #3020. - - *Jon Leighton* - -* 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/sqlite_adapter.rb: ditto - *Aaron Patterson* - -* Support bulk change_table in mysql2 adapter, as well as the mysql one. *Jon Leighton* - -* If multiple parameters are sent representing a date, and some are blank, the - resulting object is nil. In previous releases those values defaulted to 1. This only affects existing but blank parameters, missing ones still raise an error. [Akira Matsuda] -* ActiveRecord::Base.establish_connection now takes a string that contains - a URI that specifies the connection configuration. For example: - ActiveRecord::Base.establish_connection 'postgres://localhost/foo' - -## Rails 3.1.0 (August 30, 2011) ## - -* Add a proxy_association method to association proxies, which can be called by association - extensions to access information about the association. This replaces proxy_owner etc with - proxy_association.owner. - - *Jon Leighton* - -* ActiveRecord::MacroReflection::AssociationReflection#build_record has a new method signature. - - Before: def build_association(*options) - After: def build_association(*options, &block) - - Users who are redefining this method to extend functionality should ensure that the block is - passed through to ActiveRecord::Base#new. - - This change is necessary to fix https://github.com/rails/rails/issues/1842. - - *Jon Leighton* - -* AR#pluralize_table_names can be used to singularize/pluralize table name of an individual model: - - class User < ActiveRecord::Base - self.pluralize_table_names = false - end - - Previously this could only be set globally for all models through ActiveRecord::Base.pluralize_table_names. *Guillermo Iguaran* - -* Add block setting of attributes to singular associations: - - class User < ActiveRecord::Base - has_one :account - end - - user.build_account{ |a| a.credit_limit = 100.0 } - - The block is called after the instance has been initialized. *Andrew White* - -* Add ActiveRecord::Base.attribute_names to return a list of attribute names. This will return an empty array if the model is abstract or table does not exists. *Prem Sichanugrist* - -* CSV Fixtures are deprecated and support will be removed in Rails 3.2.0 +* Do not consider the numeric attribute as changed if the old value is zero and the new value + is not a string. + Fixes #7237. -* AR#new, AR#create, AR#create!, AR#update_attributes and AR#update_attributes! all accept a second hash as option that allows you - to specify which role to consider when assigning attributes. This is built on top of ActiveModel's - new mass assignment capabilities: - - class Post < ActiveRecord::Base - attr_accessible :title - attr_accessible :title, :published_at, :as => :admin - end - - Post.new(params[:post], :as => :admin) - - assign_attributes() with similar API was also added and attributes=(params, guard) was deprecated. - - Please note that this changes the method signatures for AR#new, AR#create, AR#create!, AR#update_attributes and AR#update_attributes!. If you have overwritten these methods you should update them accordingly. - - *Josh Kalderimis* - -* default_scope can take a block, lambda, or any other object which responds to `call` for lazy - evaluation: - - default_scope { ... } - default_scope lambda { ... } - default_scope method(:foo) - - This feature was originally implemented by Tim Morgan, but was then removed in favour of - defining a 'default_scope' class method, but has now been added back in by Jon Leighton. - The relevant lighthouse ticket is #1812. - -* Default scopes are now evaluated at the latest possible moment, to avoid problems where - scopes would be created which would implicitly contain the default scope, which would then - be impossible to get rid of via Model.unscoped. - - Note that this means that if you are inspecting the internal structure of an - ActiveRecord::Relation, it will *not* contain the default scope, though the resulting - query will do. You can get a relation containing the default scope by calling - ActiveRecord#with_default_scope, though this is not part of the public API. - - *Jon Leighton* - -* If you wish to merge default scopes in special ways, it is recommended to define your default - scope as a class method and use the standard techniques for sharing code (inheritance, mixins, - etc.): - - class Post < ActiveRecord::Base - def self.default_scope - where(:published => true).where(:hidden => false) - end - end - - *Jon Leighton* - -* PostgreSQL adapter only supports PostgreSQL version 8.2 and higher. - -* ConnectionManagement middleware is changed to clean up the connection pool - after the rack body has been flushed. - -* Added an update_column method on ActiveRecord. This new method updates a given attribute on an object, skipping validations and callbacks. - It is recommended to use #update_attribute unless you are sure you do not want to execute any callback, including the modification of - the updated_at column. It should not be called on new records. - Example: - - User.first.update_column(:name, "sebastian") # => true - - *Sebastian Martinez* - -* Associations with a :through option can now use *any* association as the - through or source association, including other associations which have a - :through option and has_and_belongs_to_many associations - - *Jon Leighton* - -* The configuration for the current database connection is now accessible via - ActiveRecord::Base.connection_config. *fxn* - -* limits and offsets are removed from COUNT queries unless both are supplied. - For example: - - People.limit(1).count # => 'SELECT COUNT(*) FROM people' - People.offset(1).count # => 'SELECT COUNT(*) FROM people' - People.limit(1).offset(1).count # => 'SELECT COUNT(*) FROM people LIMIT 1 OFFSET 1' + *Rafael Mendonça França* - *lighthouse #6262* +* Removes the deprecation of `update_attribute`. *fxn* -* ActiveRecord::Associations::AssociationProxy has been split. There is now an Association class - (and subclasses) which are responsible for operating on associations, and then a separate, - thin wrapper called CollectionProxy, which proxies collection associations. +* Reverted the deprecation of `composed_of`. *Rafael Mendonça França* - This prevents namespace pollution, separates concerns, and will allow further refactorings. +* Reverted the deprecation of `*_sql` association options. They will + be deprecated in 4.0 instead. *Jon Leighton* - Singular associations (has_one, belongs_to) no longer have a proxy at all. They simply return - the associated record or nil. This means that you should not use undocumented methods such - as bob.mother.create - use bob.create_mother instead. +* 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 - *Jon Leighton* + *Xavier Noria* -* Make has_many :through associations work correctly when you build a record and then save it. This - requires you to set the :inverse_of option on the source reflection on the join model, like so: +* 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. - class Post < ActiveRecord::Base - has_many :taggings - has_many :tags, :through => :taggings - end + *Rafael Mendonça França* - class Tagging < ActiveRecord::Base - belongs_to :post - belongs_to :tag, :inverse_of => :tagging # :inverse_of must be set! - end - class Tag < ActiveRecord::Base - has_many :taggings - has_many :posts, :through => :taggings - end +## Rails 3.2.7 (Jul 26, 2012) ## - post = Post.first - tag = post.tags.build :name => "ruby" - tag.save # will save a Taggable linking to the post +* `:finder_sql` and `:counter_sql` options on collection associations + are deprecated. Please transition to using scopes. *Jon Leighton* -* Support the :dependent option on has_many :through associations. For historical and practical - reasons, :delete_all is the default deletion strategy employed by association.delete(*records), - despite the fact that the default strategy is :nullify for regular has_many. Also, this only - works at all if the source reflection is a belongs_to. For other situations, you should directly - modify the through association. +* `:insert_sql` and `:delete_sql` options on `has_and_belongs_to_many` + associations are deprecated. Please transition to using `has_many + :through` *Jon Leighton* -* Changed the behaviour of association.destroy for has_and_belongs_to_many and has_many :through. - From now on, 'destroy' or 'delete' on an association will be taken to mean 'get rid of the link', - not (necessarily) 'get rid of the associated records'. - - Previously, has_and_belongs_to_many.destroy(*records) would destroy the records themselves. It - would not delete any records in the join table. Now, it deletes the records in the join table. - - Previously, has_many_through.destroy(*records) would destroy the records themselves, and the - records in the join table. [Note: This has not always been the case; previous version of Rails - only deleted the records themselves.] Now, it destroys only the records in the join table. +* `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. - Note that this change is backwards-incompatible to an extent, but there is unfortunately no - way to 'deprecate' it before changing it. The change is being made in order to have - consistency as to the meaning of 'destroy' or 'delete' across the different types of associations. + *Steve Klabnik* - If you wish to destroy the records themselves, you can do records.association.each(&:destroy) +* `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`. - *Jon Leighton* + *Steve Klabnik* -* Add :bulk => true option to change_table to make all the schema changes defined in change_table block using a single ALTER statement. *Pratik Naik* - Example: +## Rails 3.2.6 (Jun 12, 2012) ## - change_table(:users, :bulk => true) do |t| - t.string :company_name - t.change :birthdate, :datetime - end +* 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. - This will now result in: + CVE-2012-2695 - ALTER TABLE `users` ADD COLUMN `company_name` varchar(255), CHANGE `updated_at` `updated_at` datetime DEFAULT NULL +* 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. -* Removed support for accessing attributes on a has_and_belongs_to_many join table. This has been - documented as deprecated behaviour since April 2006. Please use has_many :through instead. *Jon Leighton* -* Added a create_association! method for has_one and belongs_to associations. *Jon Leighton* - -* Migration files generated from model and constructive migration generators - (for example, add_name_to_users) use the reversible migration's `change` - method instead of the ordinary `up` and `down` methods. *Prem Sichanugrist* - -* Removed support for interpolating string SQL conditions on associations. Instead, you should - use a proc, like so: - - Before: - - has_many :things, :conditions => 'foo = #{bar}' - - After: - - has_many :things, :conditions => proc { "foo = #{bar}" } - - Inside the proc, 'self' is the object which is the owner of the association, unless you are - eager loading the association, in which case 'self' is the class which the association is within. - - You can have any "normal" conditions inside the proc, so the following will work too: - - has_many :things, :conditions => proc { ["foo = ?", bar] } - - Previously :insert_sql and :delete_sql on has_and_belongs_to_many association allowed you to call - 'record' to get the record being inserted or deleted. This is now passed as an argument to - the proc. - -* Added ActiveRecord::Base#has_secure_password (via ActiveModel::SecurePassword) to encapsulate dead-simple password usage with BCrypt encryption and salting [DHH]. Example: - - # Schema: User(name:string, password_digest:string, password_salt:string) - class User < ActiveRecord::Base - has_secure_password - end - - user = User.new(:name => "david", :password => "", :password_confirmation => "nomatch") - user.save # => false, password required - user.password = "mUc3m00RsqyRe" - user.save # => false, confirmation doesn't match - user.password_confirmation = "mUc3m00RsqyRe" - user.save # => true - user.authenticate("notright") # => false - user.authenticate("mUc3m00RsqyRe") # => user - User.find_by_name("david").try(:authenticate, "notright") # => nil - User.find_by_name("david").try(:authenticate, "mUc3m00RsqyRe") # => user - - -* When a model is generated add_index is added by default for belongs_to or references columns - - rails g model post user:belongs_to will generate the following: - - class CreatePosts < ActiveRecord::Migration - def up - create_table :posts do |t| - t.belongs_to :user - t.timestamps - end - - add_index :posts, :user_id - end - - def down - drop_table :posts - end - end - - *Santiago Pastorino* - -* Setting the id of a belongs_to object will update the reference to the - object. [#2989 state:resolved] -* ActiveRecord::Base#dup and ActiveRecord::Base#clone semantics have changed - to closer match normal Ruby dup and clone semantics. -* Calling ActiveRecord::Base#clone will result in a shallow copy of the record, - including copying the frozen state. No callbacks will be called. -* Calling ActiveRecord::Base#dup will duplicate the record, including calling - after initialize hooks. Frozen state will not be copied, and all associations will be cleared. A duped record will return true for new_record?, have a nil id field, and is saveable. -* Migrations can be defined as reversible, meaning that the migration system - will figure out how to reverse your migration. To use reversible migrations, just define the "change" method. For example: - class MyMigration < ActiveRecord::Migration - def change - create_table(:horses) do - t.column :content, :text - t.column :remind_at, :datetime - end - end - end - - Some things cannot be automatically reversed for you. If you know how to reverse those things, you should define 'up' and 'down' in your migration. If you define something in `change` that cannot be reversed, an IrreversibleMigration exception will be raised when going down. -* Migrations should use instance methods rather than class methods: - class FooMigration < ActiveRecord::Migration - def up - ... - end - end - - *Aaron Patterson* - -* has_one maintains the association with separate after_create/after_update instead - of a single after_save. *fxn* - -* The following code: - - Model.limit(10).scoping { Model.count } - - now generates the following SQL: - - SELECT COUNT(*) FROM models LIMIT 10 - - This may not return what you want. Instead, you may with to do something - like this: - - Model.limit(10).scoping { Model.all.size } - - *Aaron Patterson* - - -## Rails 3.0.12 (March 1, 2012) ## - -* No changes. - - -## Rails 3.0.11 (November 18, 2011) ## - -* Exceptions from database adapters should not lose their backtrace. - -* Backport "ActiveRecord::Persistence#touch should not use default_scope" (GH #1519) - -* Psych errors with poor yaml formatting are proxied. Fixes GH #2645 and - GH #2731 - -* Fix ActiveRecord#exists? when passsed a nil value - - -## Rails 3.0.10 (August 16, 2011) ## - -* Magic encoding comment added to schema.rb files - -* schema.rb is written as UTF-8 by default. - -* Ensuring an established connection when running `rake db:schema:dump` - -* Association conditions will not clobber join conditions. -* Destroying a record will destroy the HABTM record before destroying itself. - GH #402. - -* Make `ActiveRecord::Batches#find_each` to not return `self`. - -* Update `table_exists?` in PG to to always use current search_path or schema if explictly set. - - -## Rails 3.0.9 (June 16, 2011) ## - -* No changes. - - -## Rails 3.0.8 (June 7, 2011) ## - -* Fix various problems with using :primary_key and :foreign_key options in conjunction with - :through associations. [Jon Leighton] - -* Correctly handle inner joins on polymorphic relationships. - -* Fixed infinity and negative infinity cases in PG date columns. - -* Creating records with invalid associations via `create` or `save` will no longer raise exceptions. - - -## Rails 3.0.7 (April 18, 2011) ## - -* Destroying records via nested attributes works independent of reject_if LH #6006 *Durran Jordan* - -* Delegate any? and many? to Model.scoped for consistency *Andrew White* +## Rails 3.2.5 (Jun 1, 2012) ## -* Quote the ORDER BY clause in batched finds - fixes #6620 *Andrew White* +* Restore behavior of Active Record 3.2.3 scopes. + A series of commits relating to preloading and scopes caused a regression. -* Change exists? so records are not instantiated - fixes #6127. This prevents after_find - and after_initialize callbacks being triggered when checking for record existence. *Andrew White* -* Fix performance bug with attribute accessors which only occurred on Ruby 1.8.7, and ensure we - cache type-casted values when the column returned from the db contains non-standard chars. - *Jon Leighton* - -* Fix a performance regression introduced here 86acbf1cc050c8fa8c74a10c735e467fb6fd7df8 - related to read_attribute method *Stian Grytøyr* - - -## Rails 3.0.6 (April 5, 2011) ## - -* Un-deprecate reorder method *Sebastian Martinez* - -* Extensions are applied when calling +except+ or +only+ on relations. - Thanks to Iain Hecker. - -* Schemas set in set_table_name are respected by the mysql adapter. LH #5322 - -* Fixed a bug when empty? was called on a grouped Relation that wasn't loaded. - LH #5829 - -* Reapply extensions when using except and only. Thanks Iain Hecker. - -* Binary data is escaped when being inserted to SQLite3 Databases. Thanks - Naruse! - - -## Rails 3.0.5 (February 26, 2011) ## - -* Model.where(:column => 1).where(:column => 2) will always produce an AND - query. - *Aaron Patterson* - -* Deprecated support for interpolated association conditions in the form of :conditions => 'foo = #{bar}'. - - Instead, you should use a proc, like so: - - Before: - - has_many :things, :conditions => 'foo = #{bar}' - - After: - - has_many :things, :conditions => proc { "foo = #{bar}" } - - Inside the proc, 'self' is the object which is the owner of the association, unless you are - eager loading the association, in which case 'self' is the class which the association is within. - - You can have any "normal" conditions inside the proc, so the following will work too: - - has_many :things, :conditions => proc { ["foo = ?", bar] } - - Previously :insert_sql and :delete_sql on has_and_belongs_to_many association allowed you to call - 'record' to get the record being inserted or deleted. This is now passed as an argument to - the proc. - - *Jon Leighton* - - -## Rails 3.0.4 (February 8, 2011) ## - -* Added deprecation warning for has_and_belongs_to_many associations where the join table has - additional attributes other than the keys. Access to these attributes is removed in 3.1. - Please use has_many :through instead. *Jon Leighton* - - -## Rails 3.0.3 (November 16, 2010) ## - -* Support find by class like this: Post.where(:name => Post) - - -## Rails 3.0.2 (November 15, 2010) ## - -* Dramatic speed increase (see: http://engineering.attinteractive.com/2010/10/arel-two-point-ohhhhh-yaaaaaa/) *Aaron Patterson* - -* reorder is deprecated in favor of except(:order).order(...) *Santiago Pastorino* - -* except is now AR public API - - Model.order('name').except(:order).order('salary') - - generates: - - SELECT * FROM models ORDER BY salary - - *Santiago Pastorino* - -* The following code: - - Model.limit(10).scoping { Model.count } - - now generates the following SQL: - - SELECT COUNT(*) FROM models LIMIT 10 - - This may not return what you want. Instead, you may with to do something - like this: - - Model.limit(10).scoping { Model.all.size } - - *Aaron Patterson* - - -## Rails 3.0.1 (October 15, 2010) ## - -* Introduce a fix for CVE-2010-3993 - - -## Rails 3.0.0 (August 29, 2010) ## - -* 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* - -* select and order for ActiveRecord now always concatenate nested calls. Use reorder if you want the original order to be overwritten *Santiago Pastorino* - -* PostgreSQL: ensure the database time zone matches Ruby's time zone #4895 *Aaron Patterson* - -* Fixed that ActiveRecord::Base.compute_type would swallow NoMethodError #4751 *Andrew Bloomgarden, Andrew White* - -* Add index length support for MySQL. #1852 *Emili Parreno, Pratik Naik* - - Example: - - add_index(:accounts, :name, :name => 'by_name', :length => 10) - => CREATE INDEX by_name ON accounts(name(10)) - - add_index(:accounts, [:name, :surname], :name => 'by_name_surname', :length => {:name => 10, :surname => 15}) - => CREATE INDEX by_name_surname ON accounts(name(10), surname(15)) - -* find_or_create_by_attr(value, ...) works when attr is protected. #4457 *Santiago Pastorino, Marc-André Lafortune* - -* New callbacks: after_commit and after_rollback. Do expensive operations like image thumbnailing after_commit instead of after_save. #2991 *Brian Durand* - -* Serialized attributes are not converted to YAML if they are any of the formats that can be serialized to XML (like Hash, Array and Strings). *José Valim* - -* Destroy uses optimistic locking. If lock_version on the record you're destroying doesn't match lock_version in the database, a StaleObjectError is raised. #1966 *Curtis Hawthorne* - -* PostgreSQL: drop support for old postgres driver. Use pg 0.9.0 or later. *Jeremy Kemper* - -* Observers can prevent records from saving by returning false, just like before_save and friends. #4087 *Mislav Marohnić* - -* Add Relation extensions. *Pratik Naik* - - users = User.where(:admin => true).extending(User::AdminPowers) - - latest_users = User.order('created_at DESC') do - def posts_count - Post.count(:user_id => to_a.map(&:id)) - end - end - -* To prefix the table names of all models in a module, define self.table_name_prefix on the module. #4032 *Andrew White* - -* Silenced "SHOW FIELDS" and "SET SQL_AUTO_IS_NULL=0" statements from the MySQL driver to improve log signal to noise ration in development *DHH* - -* PostgreSQLAdapter: set time_zone to UTC when Base.default_timezone == :utc so that Postgres doesn't incorrectly offset-adjust values inserted into TIMESTAMP WITH TIME ZONE columns. #3777 *Jack Christensen* - -* Allow relations to be used as scope. - - class Item - scope :red, where(:colour => 'red') - end - - Item.red.limit(10) # Ten red items - -* Rename named_scope to scope. *Pratik Naik* - -* Changed ActiveRecord::Base.store_full_sti_class to be true by default reflecting the previously announced Rails 3 default *DHH* - -* Add Relation#except. *Pratik Naik* - - one_red_item = Item.where(:colour => 'red').limit(1) - all_items = one_red_item.except(:where, :limit) - -* Add Relation#delete_all. *Pratik Naik* - - Item.where(:colour => 'red').delete_all - -* Add Model.having and Relation#having. *Pratik Naik* - - Developer.group("salary").having("sum(salary) > 10000").select("salary") - -* Add Relation#count. *Pratik Naik* - - legends = People.where("age > 100") - legends.count - legends.count(:age, :distinct => true) - legends.select('id').count - -* Add Model.readonly and association_collection#readonly finder method. *Pratik Naik* - - Post.readonly.to_a # Load all posts in readonly mode - @user.items.readonly(false).to_a # Load all the user items in writable mode - -* Add .lock finder method *Pratik Naik* - - User.lock.where(:name => 'lifo').to_a - - old_items = Item.where("age > 100") - old_items.lock.each {|i| .. } - -* Add Model.from and association_collection#from finder methods *Pratik Naik* - - user = User.scoped - user.select('*').from('users, items') - -* Add relation.destroy_all *Pratik Naik* - - old_items = Item.where("age > 100") - old_items.destroy_all - -* Add relation.exists? *Pratik Naik* - - red_items = Item.where(:colours => 'red') - red_items.exists? - red_items.exists?(1) - -* Add find(ids) to relations. *Pratik Naik* - - old_users = User.order("age DESC") - old_users.find(1) - old_users.find(1, 2, 3) - -* Add new finder methods to association collection. *Pratik Naik* - - class User < ActiveRecord::Base - has_many :items - end - - user = User.first - user.items.where(:items => {:colour => 'red'}) - user.items.select('items.id') - -* Add relation.reload to force reloading the records. *Pratik Naik* - - topics = Topic.scoped - topics.to_a # force load - topics.first # returns a cached record - topics.reload - topics.first # Fetches a new record from the database - -* Rename Model.conditions and relation.conditions to .where. *Pratik Naik* - - Before : - User.conditions(:name => 'lifo') - User.select('id').conditions(["age > ?", 21]) - - Now : - User.where(:name => 'lifo') - User.select('id').where(["age > ?", 21]) - -* Add Model.select/group/order/limit/joins/conditions/preload/eager_load class methods returning a lazy relation. *Pratik Naik* - - Examples : - - posts = Post.select('id).order('name') # Returns a lazy relation - posts.each {|p| puts p.id } # Fires "select id from posts order by name" - -* Model.scoped now returns a relation if invoked without any arguments. *Pratik Naik* - - Example : - - posts = Post.scoped - 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 - -* Association inverses for belongs_to, has_one, and has_many. Optimization to reduce database queries. #3533 *Murray Steele* - - # post.comments sets each comment's post without needing to :include - class Post < ActiveRecord::Base - has_many :comments, :inverse_of => :post - end - -* MySQL: add_ and change_column support positioning. #3286 *Ben Marini* - -* Reset your Active Record counter caches with the reset_counter_cache class method. #1211 *Mike Breen, Gabe da Silveira* - -* Remove support for SQLite 2. Please upgrade to SQLite 3+ or install the plugin from git://github.com/rails/sqlite2_adapter.git *Pratik Naik* - -* PostgreSQL: XML datatype support. #1874 *Leonardo Borges* - -* quoted_date converts time-like objects to ActiveRecord::Base.default_timezone before serialization. This allows you to use Time.now in find conditions and have it correctly be serialized as the current time in UTC when default_timezone == :utc. #2946 *Geoff Buesing* - -* SQLite: drop support for 'dbfile' option in favor of 'database.' #2363 *Paul Hinze, Jeremy Kemper* - -* Added :primary_key option to belongs_to associations. #765 *Szymon Nowak, Philip Hallstrom, Noel Rocha* - # employees.company_name references companies.name - Employee.belongs_to :company, :primary_key => 'name', :foreign_key => 'company_name' - -* Implement #many? for NamedScope and AssociationCollection using #size. #1500 *Chris Kampmeier* - -* Added :touch option to belongs_to associations that will touch the parent record when the current record is saved or destroyed *DHH* - -* Added ActiveRecord::Base#touch to update the updated_at/on attributes (or another specified timestamp) with the current time *DHH* - - -## 2.3.2 Final (March 15, 2009) ## - -* Added ActiveRecord::Base.find_each and ActiveRecord::Base.find_in_batches for batch processing *DHH/Jamis Buck* - -* Added that ActiveRecord::Base.exists? can be called with no arguments #1817 *Scott Taylor* - -* Add Support for updating deeply nested models from a single form. #1202 *Eloy Duran* - - class Book < ActiveRecord::Base - has_one :author - has_many :pages - - accepts_nested_attributes_for :author, :pages - end - -* Make after_save callbacks fire only if the record was successfully saved. #1735 *Michael Lovitt* - - Previously the callbacks would fire if a before_save cancelled saving. - -* Support nested transactions using database savepoints. #383 *Jonathan Viney, Hongli Lai* - -* Added dynamic scopes ala dynamic finders #1648 *Yaroslav Markin* - -* Fixed that ActiveRecord::Base#new_record? should return false (not nil) for existing records #1219 *Yaroslav Markin* - -* I18n the word separator for error messages. Introduces the activerecord.errors.format.separator translation key. #1294 *Akira Matsuda* - -* Add :having as a key to find and the relevant associations. *Emilio Tagua* - -* Added default_scope to Base #1381 [Paweł Kondzior]. Example: - - class Person < ActiveRecord::Base - default_scope :order => 'last_name, first_name' - end - - class Company < ActiveRecord::Base - has_many :people - end - - Person.all # => Person.find(:all, :order => 'last_name, first_name') - Company.find(1).people # => Person.find(:all, :order => 'last_name, first_name', :conditions => { :company_id => 1 }) - - -## 2.2.1 RC2 (November 14th, 2008) ## - -* Ensure indices don't flip order in schema.rb #1266 *Jordi Bunster* - -* Fixed that serialized strings should never be type-casted (i.e. turning "Yes" to a boolean) #857 *Andreas Korth* - - -## 2.2.0 RC1 (October 24th, 2008) ## - -* Skip collection ids reader optimization if using :finder_sql *Jeremy Kemper* - -* Add Model#delete instance method, similar to Model.delete class method. #1086 *Hongli Lai (Phusion)* - -* MySQL: cope with quirky default values for not-null text columns. #1043 *Frederick Cheung* - -* Multiparameter attributes skip time zone conversion for time-only columns [#1030 state:resolved] *Geoff Buesing* - -* Base.skip_time_zone_conversion_for_attributes uses class_inheritable_accessor, so that subclasses don't overwrite Base [#346 state:resolved] *Emilio Tagua* - -* Added find_last_by dynamic finder #762 *Emilio Tagua* - -* Internal API: configurable association options and build_association method for reflections so plugins may extend and override. #985 *Hongli Lai (Phusion)* - -* Changed benchmarks to be reported in milliseconds *David Heinemeier Hansson* - -* Connection pooling. #936 *Nick Sieger* - -* Merge scoped :joins together instead of overwriting them. May expose scoping bugs in your code! #501 *Andrew White* - -* before_save, before_validation and before_destroy callbacks that return false will now ROLLBACK the transaction. Previously this would have been committed before the processing was aborted. #891 *Xavier Noria* - -* Transactional migrations for databases which support them. #834 *divoxx, Adam Wiggins, Tarmo Tänav* - -* Set config.active_record.timestamped_migrations = false to have migrations with numeric prefix instead of UTC timestamp. #446. *Andrew Stone, Nik Wakelin* - -* change_column_default preserves the not-null constraint. #617 *Tarmo Tänav* - -* Fixed that create database statements would always include "DEFAULT NULL" (Nick Sieger) *#334* - -* Add :tokenizer option to validates_length_of to specify how to split up the attribute string. #507. [David Lowenfels] Example : - - \# Ensure essay contains at least 100 words. - validates_length_of :essay, :minimum => 100, :too_short => "Your essay must be at least %d words."), :tokenizer => lambda {|str| str.scan(/\w+/) } - -* Allow conditions on multiple tables to be specified using hash. [Pratik Naik]. Example: - - User.all :joins => :items, :conditions => { :age => 10, :items => { :color => 'black' } } - Item.first :conditions => { :items => { :color => 'red' } } - -* Always treat integer :limit as byte length. #420 *Tarmo Tänav* - -* Partial updates don't update lock_version if nothing changed. #426 *Daniel Morrison* - -* Fix column collision with named_scope and :joins. #46 *Duncan Beevers, Mark Catley* - -* db:migrate:down and :up update schema_migrations. #369 *Michael Raidel, RaceCondition* - -* PostgreSQL: support :conditions => [':foo::integer', { :foo => 1 }] without treating the ::integer typecast as a bind variable. *Tarmo Tänav* - -* MySQL: rename_column preserves column defaults. #466 *Diego Algorta* - -* Add :from option to calculations. #397 *Ben Munat* - -* Add :validate option to associations to enable/disable the automatic validation of associated models. Resolves #301. *Jan De Poorter* - -* PostgreSQL: use 'INSERT ... RETURNING id' for 8.2 and later. *Jeremy Kemper* - -* Added SQL escaping for :limit and :offset in MySQL *Jonathan Wiess* - - -## 2.1.0 (May 31st, 2008) ## - -* Add ActiveRecord::Base.sti_name that checks ActiveRecord::Base#store_full_sti_class? and returns either the full or demodulized name. *Rick Olson* - -* Add first/last methods to associations/named_scope. Resolved #226. *Ryan Bates* - -* Added SQL escaping for :limit and :offset #288 *Aaron Bedra, Steven Bristol, Jonathan Wiess* - -* Added first/last methods to associations/named_scope. Resolved #226. *Ryan Bates* - -* Ensure hm:t preloading honours reflection options. Resolves #137. *Frederick Cheung* - -* Added protection against duplicate migration names (Aslak Hellesøy) *#112* - -* Base#instantiate_time_object: eliminate check for Time.zone, since we can assume this is set if time_zone_aware_attributes is set to true *Geoff Buesing* - -* Time zone aware attribute methods use Time.zone.parse instead of #to_time for String arguments, so that offset information in String is respected. Resolves #105. *Scott Fleckenstein, Geoff Buesing* - -* Added change_table for migrations (Jeff Dean) [#71]. Example: - - change_table :videos do |t| - t.timestamps # adds created_at, updated_at - t.belongs_to :goat # adds goat_id integer - t.string :name, :email, :limit => 20 # adds name and email both with a 20 char limit - t.remove :name, :email # removes the name and email columns - end - -* Fixed has_many :through .create with no parameters caused a "can't dup NilClass" error (Steven Soroka) *#85* - -* Added block-setting of attributes for Base.create like Base.new already has (Adam Meehan) *#39* - -* Fixed that pessimistic locking you reference the quoted table name (Josh Susser) *#67* - -* Fixed that change_column should be able to use :null => true on a field that formerly had false [Nate Wiger] *#26* - -* Added that the MySQL adapter should map integer to either smallint, int, or bigint depending on the :limit just like PostgreSQL *David Heinemeier Hansson* - -* Change validates_uniqueness_of :case_sensitive option default back to true (from [9160]). Love your database columns, don't LOWER them. *Rick Olson* - -* Add support for interleaving migrations by storing which migrations have run in the new schema_migrations table. Closes #11493 *Jordi Bunster* - -* ActiveRecord::Base#sum defaults to 0 if no rows are returned. Closes #11550 *Kamal Fariz Mahyuddin* - -* Ensure that respond_to? considers dynamic finder methods. Closes #11538. *James Mead* - -* Ensure that save on parent object fails for invalid has_one association. Closes #10518. *Pratik Naik* - -* Remove duplicate code from associations. *Pratik Naik* - -* Refactor HasManyThroughAssociation to inherit from HasManyAssociation. Association callbacks and <association>_ids= now work with hm:t. #11516 *Ruy Asan* - -* Ensure HABTM#create and HABTM#build do not load entire association. *Pratik Naik* - -* Improve documentation. *Xavier Noria, Jack Danger Canty, leethal* - -* Tweak ActiveRecord::Base#to_json to include a root value in the returned hash: {"post": {"title": ...}} *Rick Olson* - - Post.find(1).to_json # => {"title": ...} - config.active_record.include_root_in_json = true - Post.find(1).to_json # => {"post": {"title": ...}} - -* Add efficient #include? to AssociationCollection (for has_many/has_many :through/habtm). *stopdropandrew* - -* PostgreSQL: create_ and drop_database support. #9042 *ez, pedz, Nick Sieger* - -* Ensure that validates_uniqueness_of works with with_scope. Closes #9235. *Nik Wakelin, cavalle* - -* Partial updates include only unsaved attributes. Off by default; set YourClass.partial_updates = true to enable. *Jeremy Kemper* - -* Removing unnecessary uses_tzinfo helper from tests, given that TZInfo is now bundled *Geoff Buesing* - -* Fixed that validates_size_of :within works in associations #11295, #10019 *cavalle* - -* Track changes to unsaved attributes. *Jeremy Kemper* - -* Switched to UTC-timebased version numbers for migrations and the schema. This will as good as eliminate the problem of multiple migrations getting the same version assigned in different branches. Also added rake db:migrate:up/down to apply individual migrations that may need to be run when you merge branches #11458 *John Barnette* - -* Fixed that has_many :through would ignore the hash conditions #11447 *Emilio Tagua* - -* Fix issue where the :uniq option of a has_many :through association is ignored when find(:all) is called. Closes #9407 *cavalle* - -* Fix duplicate table alias error when including an association with a has_many :through association on the same join table. Closes #7310 *cavalle* - -* More efficient association preloading code that compacts a through_records array in a central location. Closes #11427 *Jack Danger Canty* - -* Improve documentation. *Ryan Bigg, Jan De Poorter, Cheah Chu Yeow, Xavier Shay, Jack Danger Canty, Emilio Tagua, Xavier Noria, Sunny Ripert* - -* Fixed that ActiveRecord#Base.find_or_create/initialize would not honor attr_protected/accessible when used with a hash #11422 *Emilio Tagua* - -* Added ActiveRecord#Base.all/first/last as aliases for find(:all/:first/:last) #11413 *nkallen, Chris O'Sullivan* - -* Merge the has_finder gem, renamed as 'named_scope'. #11404 *nkallen* - - class Article < ActiveRecord::Base - named_scope :published, :conditions => {:published => true} - named_scope :popular, :conditions => ... - end - - Article.published.paginate(:page => 1) - Article.published.popular.count - Article.popular.find(:first) - Article.popular.find(:all, :conditions => {...}) - - See http://pivots.pivotallabs.com/users/nick/blog/articles/284-hasfinder-it-s-now-easier-than-ever-to-create-complex-re-usable-sql-queries - -* Add has_one :through support. #4756 *Chris O'Sullivan* - -* Migrations: create_table supports primary_key_prefix_type. #10314 *student, Chris O'Sullivan* - -* Added logging for dependency load errors with fixtures #11056 *stuthulhu* - -* Time zone aware attributes use Time#in_time_zone *Geoff Buesing* - -* Fixed that scoped joins would not always be respected #6821 *Theory/Jack Danger Canty* - -* Ensure that ActiveRecord::Calculations disambiguates field names with the table name. #11027 *cavalle* - -* Added add/remove_timestamps to the schema statements for adding the created_at/updated_at columns on existing tables #11129 *jramirez* - -* Added ActiveRecord::Base.find(:last) #11338 *Emilio Tagua* - -* test_native_types expects DateTime.local_offset instead of DateTime.now.offset; fixes test breakage due to dst transition *Geoff Buesing* - -* Add :readonly option to HasManyThrough associations. #11156 *Emilio Tagua* - -* Improve performance on :include/:conditions/:limit queries by selectively joining in the pre-query. #9560 *dasil003* - -* Perf fix: Avoid the use of named block arguments. Closes #11109 *adymo* - -* PostgreSQL: support server versions 7.4 through 8.0 and the ruby-pg driver. #11127 *jdavis* - -* Ensure association preloading doesn't break when an association returns nil. ##11145 *GMFlash* - -* Make dynamic finders respect the :include on HasManyThrough associations. #10998. *cpytel* - -* Base#instantiate_time_object only uses Time.zone when Base.time_zone_aware_attributes is true; leverages Time#time_with_datetime_fallback for readability *Geoff Buesing* - -* Refactor ConnectionAdapters::Column.new_time: leverage DateTime failover behavior of Time#time_with_datetime_fallback *Geoff Buesing* - -* Improve associations performance by using symbol callbacks instead of string callbacks. #11108 *adymo* - -* Optimise the BigDecimal conversion code. #11110 *adymo* - -* Introduce the :readonly option to all associations. Records from the association cannot be saved. #11084 *Emilio Tagua* - -* Multiparameter attributes for time columns fail over to DateTime when out of range of Time *Geoff Buesing* - -* Base#instantiate_time_object uses Time.zone.local() *Geoff Buesing* - -* Add timezone-aware attribute readers and writers. #10982 *Geoff Buesing* - -* Instantiating time objects in multiparameter attributes uses Time.zone if available. #10982 *Rick Olson* - -* Add note about how ActiveRecord::Observer classes are initialized in a Rails app. #10980 *Xavier Noria* - -* MySQL: omit text/blob defaults from the schema instead of using an empty string. #10963 *mdeiters* - -* belongs_to supports :dependent => :destroy and :delete. #10592 *Jonathan Viney* - -* Introduce preload query strategy for eager :includes. #9640 *Frederick Cheung, Aliaksey Kandratsenka, codafoo* - -* Support aggregations in finder conditions. #10572 *Ryan Kinderman* - -* Organize and clean up the Active Record test suite. #10742 *John Barnette* - -* Ensure that modifying has_and_belongs_to_many actions clear the query cache. Closes #10840 *john.andrews* - -* Fix issue where Table#references doesn't pass a :null option to a *_type attribute for polymorphic associations. Closes #10753 *railsjitsu* - -* Fixtures: removed support for the ancient pre-YAML file format. #10736 *John Barnette* - -* More thoroughly quote table names. #10698 *dimdenis, lotswholetime, Jeremy Kemper* - -* update_all ignores scoped :order and :limit, so post.comments.update_all doesn't try to include the comment order in the update statement. #10686 *Brendan Ribera* - -* Added ActiveRecord::Base.cache_key to make it easier to cache Active Records in combination with the new ActiveSupport::Cache::* libraries *David Heinemeier Hansson* - -* Make sure CSV fixtures are compatible with ruby 1.9's new csv implementation. *JEG2* - -* Added by parameter to increment, decrement, and their bang varieties so you can do player1.increment!(:points, 5) #10542 *Sam* - -* Optimize ActiveRecord::Base#exists? to use #select_all instead of #find. Closes #10605 *jamesh, Frederick Cheung, protocool* - -* Don't unnecessarily load has_many associations in after_update callbacks. Closes #6822 *stopdropandrew, canadaduane* - -* Eager belongs_to :include infers the foreign key from the association name rather than the class name. #10517 *Jonathan Viney* - -* SQLite: fix rename_ and remove_column for columns with unique indexes. #10576 *Brandon Keepers* - -* Ruby 1.9 compatibility. #10655 *Jeremy Kemper, Dirkjan Bussink* - - -## 2.0.2 (December 16th, 2007) ## - -* Ensure optimistic locking handles nil #lock_version values properly. Closes #10510 *Rick Olson* - -* Make the Fixtures Test::Unit enhancements more supporting for double-loaded test cases. Closes #10379 *brynary* - -* Fix that validates_acceptance_of still works for non-existent tables (useful for bootstrapping new databases). Closes #10474 *Josh Susser* - -* Ensure that the :uniq option for has_many :through associations retains the order. #10463 *remvee* - -* Base.exists? doesn't rescue exceptions to avoid hiding SQL errors. #10458 *Michael Klishin* - -* Documentation: Active Record exceptions, destroy_all and delete_all. #10444, #10447 *Michael Klishin* - - -## 2.0.1 (December 7th, 2007) ## - -* Removed query cache rescue as it could cause code to be run twice (closes #10408) *David Heinemeier Hansson* - - -## 2.0.0 (December 6th, 2007) ## - -* Anchor DateTimeTest to fixed DateTime instead of a variable value based on Time.now#advance#to_datetime, so that this test passes on 64-bit platforms running Ruby 1.8.6+ *Geoff Buesing* - -* Fixed that the Query Cache should just be ignored if the database is misconfigured (so that the "About your applications environment" works even before the database has been created) *David Heinemeier Hansson* - -* Fixed that the truncation of strings longer than 50 chars should use inspect - so newlines etc are escaped #10385 [Norbert Crombach] -* Fixed that habtm associations should be able to set :select as part of their definition and have that honored *David Heinemeier Hansson* - -* Document how the :include option can be used in Calculations::calculate. Closes #7446 *adamwiggins, ultimoamore* - -* Fix typo in documentation for polymorphic associations w/STI. Closes #7461 *johnjosephbachir* - -* Reveal that the type option in migrations can be any supported column type for your database but also include caveat about agnosticism. Closes #7531 *adamwiggins, mikong* - -* More complete documentation for find_by_sql. Closes #7912 *fearoffish* - -* Added ActiveRecord::Base#becomes to turn a record into one of another class (mostly relevant for STIs) [David Heinemeier Hansson]. Example: - - render :partial => @client.becomes(Company) # renders companies/company instead of clients/client - -* Fixed that to_xml should not automatically pass :procs to associations included with :include #10162 *Cheah Chu Yeow* - -* Fix documentation typo introduced in [8250]. Closes #10339 *Henrik N* - -* Foxy fixtures: support single-table inheritance. #10234 *tom* - -* Foxy fixtures: allow mixed usage to make migration easier and more attractive. #10004 *lotswholetime* - -* Make the record_timestamps class-inheritable so it can be set per model. #10004 *tmacedo* - -* Allow validates_acceptance_of to use a real attribute instead of only virtual (so you can record that the acceptance occured) #7457 *ambethia* - -* DateTimes use Ruby's default calendar reform setting. #10201 *Geoff Buesing* - -* Dynamic finders on association collections respect association :order and :limit. #10211, #10227 *Patrick Joyce, Rick Olson, Jack Danger Canty* - -* Add 'foxy' support for fixtures of polymorphic associations. #10183 *John Barnette, David Lowenfels* - -* validates_inclusion_of and validates_exclusion_of allow formatted :message strings. #8132 *devrieda, Mike Naberezny* - -* attr_readonly behaves well with optimistic locking. #10188 *Nick Bugajski* - -* Base#to_xml supports the nil="true" attribute like Hash#to_xml. #8268 *Jonathan del Strother* - -* Change plings to the more conventional quotes in the documentation. Closes #10104 *Jack Danger Canty* - -* Fix HasManyThrough Association so it uses :conditions on the HasMany Association. Closes #9729 *Jack Danger Canty* - -* Ensure that column names are quoted. Closes #10134 *wesley.moxam* - -* Smattering of grammatical fixes to documentation. Closes #10083 *Bob Silva* - -* Enhance explanation with more examples for attr_accessible macro. Closes #8095 *fearoffish, Marcel Molina Jr.* - -* Update association/method mapping table to refected latest collection methods for has_many :through. Closes #8772 *Pratik Naik* - -* Explain semantics of having several different AR instances in a transaction block. Closes #9036 *jacobat, Marcel Molina Jr.* - -* Update Schema documentation to use updated sexy migration notation. Closes #10086 *Sam Granieri* - -* Make fixtures work with the new test subclasses. *Tarmo Tänav, Michael Koziarski* - -* Introduce finder :joins with associations. Same :include syntax but with inner rather than outer joins. #10012 *RubyRedRick* - # Find users with an avatar - User.find(:all, :joins => :avatar) - - # Find posts with a high-rated comment. - Post.find(:all, :joins => :comments, :conditions => 'comments.rating > 3') - -* Associations: speedup duplicate record check. #10011 *Pratik Naik* - -* Make sure that << works on has_many associations on unsaved records. Closes #9989 *Josh Susser* - -* Allow association redefinition in subclasses. #9346 *wildchild* - -* Fix has_many :through delete with custom foreign keys. #6466 *naffis* - -* Foxy fixtures, from rathole (http://svn.geeksomnia.com/rathole/trunk/README) - - stable, autogenerated IDs - - specify associations (belongs_to, has_one, has_many) by label, not ID - - specify HABTM associations as inline lists - - autofill timestamp columns - - support YAML defaults - - fixture label interpolation - Enabled for fixtures that correspond to a model class and don't specify a primary key value. #9981 *John Barnette* - -* Add docs explaining how to protect all attributes using attr_accessible with no arguments. Closes #9631 *boone, rmm5t* - -* Update add_index documentation to use new options api. Closes #9787 *Kamal Fariz Mahyuddin* - -* Allow find on a has_many association defined with :finder_sql to accept id arguments as strings like regular find does. Closes #9916 *krishna* - -* Use VALID_FIND_OPTIONS when resolving :find scoping rather than hard coding the list of valid find options. Closes #9443 *sur* - -* Limited eager loading no longer ignores scoped :order. Closes #9561 *Jack Danger Canty, Josh Peek* - -* Assigning an instance of a foreign class to a composed_of aggregate calls an optional conversion block. Refactor and simplify composed_of implementation. #6322 *brandon, Chris Cruft* - -* Assigning nil to a composed_of aggregate also sets its immediate value to nil. #9843 *Chris Cruft* - -* Ensure that mysql quotes table names with database names correctly. Closes #9911 *crayz* - - "foo.bar" => "`foo`.`bar`" - -* Complete the assimilation of Sexy Migrations from ErrFree *Chris Wanstrath, PJ Hyett* - http://errtheblog.com/post/2381 - -* Qualified column names work in hash conditions, like :conditions => { 'comments.created_at' => ... }. #9733 *Jack Danger Canty* - -* Fix regression where the association would not construct new finder SQL on save causing bogus queries for "WHERE owner_id = NULL" even after owner was saved. #8713 *Bryan Helmkamp* - -* Refactor association create and build so before & after callbacks behave consistently. #8854 *Pratik Naik, mortent* - -* Quote table names. Defaults to column quoting. #4593 *Justin Lynn, gwcoffey, eadz, Dmitry V. Sabanin, Jeremy Kemper* - -* Alias association #build to #new so it behaves predictably. #8787 *Pratik Naik* - -* Add notes to documentation regarding attr_readonly behavior with counter caches and polymorphic associations. Closes #9835 *saimonmoore, Rick Olson* - -* Observers can observe model names as symbols properly now. Closes #9869 *queso* - -* find_and_(initialize|create)_by methods can now properly initialize protected attributes *Tobias Lütke* - -* belongs_to infers the foreign key from the association name instead of from the class name. *Jeremy Kemper* - -* PostgreSQL: support multiline default values. #7533 *Carl Lerche, aguynamedryan, Rein Henrichs, Tarmo Tänav* - -* MySQL: fix change_column on not-null columns that don't accept dfeault values of ''. #6663 *Jonathan Viney, Tarmo Tänav* - -* validates_uniqueness_of behaves well with abstract superclasses and - single-table inheritance. #3833, #9886 [Gabriel Gironda, rramdas, François Beausoleil, Josh Peek, Tarmo Tänav, pat] -* Warn about protected attribute assigments in development and test environments when mass-assigning to an attr_protected attribute. #9802 *Henrik N* - -* Speedup database date/time parsing. *Jeremy Kemper, Tarmo Tänav* - -* Fix calling .clear on a has_many :dependent=>:delete_all association. *Tarmo Tänav* - -* Allow change_column to set NOT NULL in the PostgreSQL adapter *Tarmo Tänav* - -* Fix that ActiveRecord would create attribute methods and override custom attribute getters if the method is also defined in Kernel.methods. *Rick Olson* - -* Don't call attr_readonly on polymorphic belongs_to associations, in case it matches the name of some other non-ActiveRecord class/module. *Rick Olson* - -* Try loading activerecord-<adaptername>-adapter gem before trying a plain require so you can use custom gems for the bundled adapters. Also stops gems from requiring an adapter from an old Active Record gem. *Jeremy Kemper, Derrick Spell* - - -## 2.0.0 Preview Release (September 29th, 2007) Includes duplicates of changes from 1.14.2 - 1.15.3 ## - -* Add attr_readonly to specify columns that are skipped during a normal ActiveRecord #save operation. Closes #6896 *Dan Manges* - - class Comment < ActiveRecord::Base - # Automatically sets Article#comments_count as readonly. - belongs_to :article, :counter_cache => :comments_count - end - - class Article < ActiveRecord::Base - attr_readonly :approved_comments_count - end - -* Make size for has_many :through use counter cache if it exists. Closes #9734 *Xavier Shay* - -* Remove DB2 adapter since IBM chooses to maintain their own adapter instead. *Jeremy Kemper* - -* Extract Oracle, SQLServer, and Sybase adapters into gems. *Jeremy Kemper* - -* Added fixture caching that'll speed up a normal fixture-powered test suite between 50% and 100% #9682 *Frederick Cheung* - -* Correctly quote id list for limited eager loading. #7482 *tmacedo* - -* Fixed that using version-targetted migrates would fail on loggers other than the default one #7430 *valeksenko* - -* Fixed rename_column for SQLite when using symbols for the column names #8616 *drodriguez* - -* Added the possibility of using symbols in addition to concrete classes with ActiveRecord::Observer#observe. #3998 *Robby Russell, Tarmo Tänav* - -* Added ActiveRecord::Base#to_json/from_json *David Heinemeier Hansson, Cheah Chu Yeow* - -* Added ActiveRecord::Base#from_xml [David Heinemeier Hansson]. Example: - - xml = "<person><name>David</name></person>" - Person.new.from_xml(xml).name # => "David" - -* Define dynamic finders as real methods after first usage. *bscofield* - -* Deprecation: remove deprecated threaded_connections methods. Use allow_concurrency instead. *Jeremy Kemper* - -* Associations macros accept extension blocks alongside modules. #9346 *Josh Peek* - -* Speed up and simplify query caching. *Jeremy Kemper* - -* connection.select_rows 'sql' returns an array (rows) of arrays (field values). #2329 *Michael Schuerig* - -* Eager loading respects explicit :joins. #9496 *dasil003* - -* Extract Firebird, FrontBase, and OpenBase adapters into gems. #9508, #9509, #9510 *Jeremy Kemper* - -* RubyGem database adapters: expects a gem named activerecord-<database>-adapter with active_record/connection_adapters/<database>_adapter.rb in its load path. *Jeremy Kemper* - -* Fixed that altering join tables in migrations would fail w/ sqlite3 #7453 *TimoMihaljov/brandon* - -* Fix association writer with :dependent => :nullify. #7314 *Jonathan Viney* - -* OpenBase: update for new lib and latest Rails. Support migrations. #8748 *dcsesq* - -* Moved acts_as_tree into a plugin of the same name on the official Rails svn. #9514 *Pratik Naik* - -* Moved acts_as_nested_set into a plugin of the same name on the official Rails svn. #9516 *Josh Peek* - -* Moved acts_as_list into a plugin of the same name on the official Rails svn. *Josh Peek* - -* Explicitly require active_record/query_cache before using it. *Jeremy Kemper* - -* Fix bug where unserializing an attribute attempts to modify a frozen @attributes hash for a deleted record. *Rick Olson, marclove* - -* Performance: absorb instantiate and initialize_with_callbacks into the Base methods. *Jeremy Kemper* - -* Fixed that eager loading queries and with_scope should respect the :group option *David Heinemeier Hansson* - -* Improve performance and functionality of the postgresql adapter. Closes #8049 *roderickvd* - - For more information see: http://dev.rubyonrails.org/ticket/8049 - -* Don't clobber includes passed to has_many.count *Jack Danger Canty* - -* Make sure has_many uses :include when counting *Jack Danger Canty* - -* Change the implementation of ActiveRecord's attribute reader and writer methods *Michael Koziarski* - - Generate Reader and Writer methods which cache attribute values in hashes. This is to avoid repeatedly parsing the same date or integer columns. - Change exception raised when users use find with :select then try to access a skipped column. Plugins could override missing_attribute() to lazily load the columns. - Move method definition to the class, instead of the instance - Always generate the readers, writers and predicate methods. -* Perform a deep #dup on query cache results so that modifying activerecord attributes does not modify the cached attributes. *Rick Olson* - - \# Ensure that has_many :through associations use a count query instead of loading the target when #size is called. Closes #8800 [Pratik Naik] -* Added :unless clause to validations #8003 [monki]. Example: - - def using_open_id? - !identity_url.blank? - end - - validates_presence_of :identity_url, :if => using_open_id? - validates_presence_of :username, :unless => using_open_id? - validates_presence_of :password, :unless => using_open_id? - -* Fix #count on a has_many :through association so that it recognizes the :uniq option. Closes #8801 *Pratik Naik* - -* Fix and properly document/test count(column_name) usage. Closes #8999 *Pratik Naik* - -* Remove deprecated count(conditions=nil, joins=nil) usage. Closes #8993 *Pratik Naik* - -* Change belongs_to so that the foreign_key assumption is taken from the association name, not the class name. Closes #8992 *Josh Susser* - - OLD - belongs_to :visitor, :class_name => 'User' # => inferred foreign_key is user_id - - NEW - belongs_to :visitor, :class_name => 'User' # => inferred foreign_key is visitor_id - -* Remove spurious tests from deprecated_associations_test, most of these aren't deprecated, and are duplicated in associations_test. Closes #8987 *Pratik Naik* - -* Make create! on a has_many :through association return the association object. Not the collection. Closes #8786 *Pratik Naik* - -* Move from select * to select tablename.* to avoid clobbering IDs. Closes #8889 *dasil003* - -* Don't call unsupported methods on associated objects when using :include, :method with to_xml #7307, *Manfred Stienstra, jwilger* - -* Define collection singular ids method for has_many :through associations. #8763 *Pratik Naik* - -* Array attribute conditions work with proxied association collections. #8318 *Kamal Fariz Mahyuddin, theamazingrando* - -* Fix polymorphic has_one associations declared in an abstract class. #8638 *Pratik Naik, Dax Huiberts* - -* Fixed validates_associated should not stop on the first error. #4276 *mrj, Manfred Stienstra, Josh Peek* - -* Rollback if commit raises an exception. #8642 *kik, Jeremy Kemper* - -* Update tests' use of fixtures for the new collections api. #8726 *Kamal Fariz Mahyuddin* - -* Save associated records only if the association is already loaded. #8713 *Blaine* - -* MySQL: fix show_variable. #8448 *matt, Jeremy Kemper* - -* Fixtures: correctly delete and insert fixtures in a single transaction. #8553 *Michael Schuerig* - -* Fixtures: people(:technomancy, :josh) returns both fixtures. #7880 *technomancy, Josh Peek* - -* Calculations support non-numeric foreign keys. #8154 *Kamal Fariz Mahyuddin* - -* with_scope is protected. #8524 *Josh Peek* - -* Quickref for association methods. #7723 *marclove, Mindsweeper* - -* Calculations: return nil average instead of 0 when there are no rows to average. #8298 *davidw* - -* acts_as_nested_set: direct_children is sorted correctly. #4761 *Josh Peek, rails@33lc0.net* - -* Raise an exception if both attr_protected and attr_accessible are declared. #8507 *stellsmi* - -* SQLite, MySQL, PostgreSQL, Oracle: quote column names in column migration SQL statements. #8466 *marclove, lorenjohnson* - -* Allow nil serialized attributes with a set class constraint. #7293 *sandofsky* - -* Oracle: support binary fixtures. #7987 *Michael Schoen* - -* Fixtures: pull fixture insertion into the database adapters. #7987 *Michael Schoen* - -* Announce migration versions as they're performed. *Jeremy Kemper* - -* find gracefully copes with blank :conditions. #7599 *Dan Manges, johnnyb* - -* validates_numericality_of takes :greater_than, :greater_than_or_equal_to, :equal_to, :less_than, :less_than_or_equal_to, :odd, and :even options. #3952 *Bob Silva, Dan Kubb, Josh Peek* - -* MySQL: create_database takes :charset and :collation options. Charset defaults to utf8. #8448 *matt* - -* Find with a list of ids supports limit/offset. #8437 *hrudududu* - -* Optimistic locking: revert the lock version when an update fails. #7840 *plang* - -* Migrations: add_column supports custom column types. #7742 *jsgarvin, Theory* - -* Load database adapters on demand. Eliminates config.connection_adapters and RAILS_CONNECTION_ADAPTERS. Add your lib directory to the $LOAD_PATH and put your custom adapter in lib/active_record/connection_adapters/adaptername_adapter.rb. This way you can provide custom adapters as plugins or gems without modifying Rails. *Jeremy Kemper* - -* Ensure that associations with :dependent => :delete_all respect :conditions option. Closes #8034 *Jack Danger Canty, Josh Peek, Rick Olson* - -* belongs_to assignment creates a new proxy rather than modifying its target in-place. #8412 *mmangino@elevatedrails.com* - -* Fix column type detection while loading fixtures. Closes #7987 *roderickvd* - -* Document deep eager includes. #6267 *Josh Susser, Dan Manges* - -* Document warning that associations names shouldn't be reserved words. #4378 *murphy@cYcnus.de, Josh Susser* - -* Sanitize Base#inspect. #8392, #8623 *Nik Wakelin, jnoon* - -* Replace the transaction {|transaction|..} semantics with a new Exception ActiveRecord::Rollback. *Michael Koziarski* - -* Oracle: extract column length for CHAR also. #7866 *ymendel* - -* Document :allow_nil option for validates_acceptance_of since it defaults to true. *tzaharia* - -* Update documentation for :dependent declaration so that it explicitly uses the non-deprecated API. *Jack Danger Canty* - -* Add documentation caveat about when to use count_by_sql. *fearoffish* - -* Enhance documentation for increment_counter and decrement_counter. *fearoffish* - -* Provide brief introduction to what optimistic locking is. *fearoffish* - -* Add documentation for :encoding option to mysql adapter. *marclove* - -* Added short-hand declaration style to migrations (inspiration from Sexy Migrations, http://errtheblog.com/post/2381) [David Heinemeier Hansson]. Example: - - create_table "products" do |t| - t.column "shop_id", :integer - t.column "creator_id", :integer - t.column "name", :string, :default => "Untitled" - t.column "value", :string, :default => "Untitled" - t.column "created_at", :datetime - t.column "updated_at", :datetime - end - - ...can now be written as: - - create_table :products do |t| - t.integer :shop_id, :creator_id - t.string :name, :value, :default => "Untitled" - t.timestamps - end - -* Use association name for the wrapper element when using .to_xml. Previous behavior lead to non-deterministic situations with STI and polymorphic associations. *Michael Koziarski, jstrachan* - -* Improve performance of calling .create on has_many :through associations. *evan* - -* Improved cloning performance by relying less on exception raising #8159 *Blaine* - -* Added ActiveRecord::Base.inspect to return a column-view like #<Post id:integer, title:string, body:text> *David Heinemeier Hansson* - -* Added yielding of Builder instance for ActiveRecord::Base#to_xml calls *David Heinemeier Hansson* - -* Small additions and fixes for ActiveRecord documentation. Closes #7342 *Jeremy McAnally* - -* Add helpful debugging info to the ActiveRecord::StatementInvalid exception in ActiveRecord::ConnectionAdapters::SqliteAdapter#table_structure. Closes #7925. *court3nay* - -* SQLite: binary escaping works with $KCODE='u'. #7862 *tsuka* - -* Base#to_xml supports serialized attributes. #7502 *jonathan* - -* Base.update_all :order and :limit options. Useful for MySQL updates that must be ordered to avoid violating unique constraints. *Jeremy Kemper* - -* Remove deprecated object transactions. People relying on this functionality should install the object_transactions plugin at http://code.bitsweat.net/svn/object_transactions. Closes #5637 *Michael Koziarski, Jeremy Kemper* - -* PostgreSQL: remove DateTime -> Time downcast. Warning: do not enable translate_results for the C bindings if you have timestamps outside Time's domain. *Jeremy Kemper* - -* find_or_create_by_* takes a hash so you can create with more attributes than are in the method name. For example, Person.find_or_create_by_name(:name => 'Henry', :comments => 'Hi new user!') is equivalent to Person.find_by_name('Henry') || Person.create(:name => 'Henry', :comments => 'Hi new user!'). #7368 *Josh Susser* - -* Make sure with_scope takes both :select and :joins into account when setting :readonly. Allows you to save records you retrieve using method_missing on a has_many :through associations. *Michael Koziarski* - -* Allow a polymorphic :source for has_many :through associations. Closes #7143 *protocool* - -* Consistent public/protected/private visibility for chained methods. #7813 *Dan Manges* - -* Oracle: fix quoted primary keys and datetime overflow. #7798 *Michael Schoen* - -* Consistently quote primary key column names. #7763 *toolmantim* - -* Fixtures: fix YAML ordered map support. #2665 *Manuel Holtgrewe, nfbuckley* - -* DateTimes assume the default timezone. #7764 *Geoff Buesing* - -* Sybase: hide timestamp columns since they're inherently read-only. #7716 *Mike Joyce* - -* Oracle: overflow Time to DateTime. #7718 *Michael Schoen* - -* PostgreSQL: don't use async_exec and async_query with postgres-pr. #7727, #7762 *flowdelic, toolmantim* - -* Fix has_many :through << with custom foreign keys. #6466, #7153 *naffis, Rich Collins* - -* Test DateTime native type in migrations, including an edge case with dates - during calendar reform. #7649, #7724 [fedot, Geoff Buesing] -* SQLServer: correctly schema-dump tables with no indexes or descending indexes. #7333, #7703 *Jakob Skjerning, Tom Ward* - -* SQLServer: recognize real column type as Ruby float. #7057 *sethladd, Tom Ward* - -* Added fixtures :all as a way of loading all fixtures in the fixture directory at once #7214 *Manfred Stienstra* - -* Added database connection as a yield parameter to ActiveRecord::Base.transaction so you can manually rollback [David Heinemeier Hansson]. Example: - - transaction do |transaction| - david.withdrawal(100) - mary.deposit(100) - transaction.rollback! # rolls back the transaction that was otherwise going to be successful - end - -* Made increment_counter/decrement_counter play nicely with optimistic locking, and added a more general update_counters method *Jamis Buck* - -* Reworked David's query cache to be available as Model.cache {...}. For the duration of the block no select query should be run more then once. Any inserts/deletes/executes will flush the whole cache however *Tobias Lütke* - Task.cache { Task.find(1); Task.find(1) } # => 1 query - -* When dealing with SQLite3, use the table_info pragma helper, so that the bindings can do some translation for when sqlite3 breaks incompatibly between point releases. *Jamis Buck* - -* Oracle: fix lob and text default handling. #7344 *gfriedrich, Michael Schoen* - -* SQLServer: don't choke on strings containing 'null'. #7083 *Jakob Skjerning* - -* MySQL: blob and text columns may not have defaults in 5.x. Update fixtures schema for strict mode. #6695 *Dan Kubb* - -* update_all can take a Hash argument. sanitize_sql splits into two methods for conditions and assignment since NULL values and delimiters are handled differently. #6583, #7365 *sandofsky, Assaf* - -* MySQL: SET SQL_AUTO_IS_NULL=0 so 'where id is null' doesn't select the last inserted id. #6778 *Jonathan Viney, timc* - -* Use Date#to_s(:db) for quoted dates. #7411 *Michael Schoen* - -* Don't create instance writer methods for class attributes. Closes #7401 *Rick Olson* - -* Docs: validations examples. #7343 *zackchandler* - -* Add missing tests ensuring callbacks work with class inheritance. Closes #7339 *sandofsky* - -* Fixtures use the table name and connection from set_fixture_class. #7330 *Anthony Eden* - -* Remove useless code in #attribute_present? since 0 != blank?. Closes #7249 *Josh Susser* - -* Fix minor doc typos. Closes #7157 *Josh Susser* - -* Fix incorrect usage of #classify when creating the eager loading join statement. Closes #7044 *Josh Susser* - -* SQLServer: quote table name in indexes query. #2928 *keithm@infused.org* - -* Subclasses of an abstract class work with single-table inheritance. #5704, #7284 *BertG, nick+rails@ag.arizona.edu* - -* Make sure sqlite3 driver closes open connections on disconnect *Rob Rasmussen* - -* [DOC] clear up some ambiguity with the way has_and_belongs_to_many creates the default join table name. #7072 *Jeremy McAnally* - -* change_column accepts :default => nil. Skip column options for primary keys. #6956, #7048 *Dan Manges, Jeremy Kemper* - -* MySQL, PostgreSQL: change_column_default quotes the default value and doesn't lose column type information. #3987, #6664 *Jonathan Viney, Manfred Stienstra, altano@bigfoot.com* - -* Oracle: create_table takes a :sequence_name option to override the 'tablename_seq' default. #7000 *Michael Schoen* - -* MySQL: retain SSL settings on reconnect. #6976 *randyv2* - -* Apply scoping during initialize instead of create. Fixes setting of foreign key when using find_or_initialize_by with scoping. *Cody Fauser* - -* SQLServer: handle [quoted] table names. #6635 *rrich* - -* acts_as_nested_set works with single-table inheritance. #6030 *Josh Susser* - -* PostgreSQL, Oracle: correctly perform eager finds with :limit and :order. #4668, #7021 *eventualbuddha, Michael Schoen* - -* Pass a range in :conditions to use the SQL BETWEEN operator. #6974 *Dan Manges* - Student.find(:all, :conditions => { :grade => 9..12 }) - -* Fix the Oracle adapter for serialized attributes stored in CLOBs. Closes #6825 *mschoen, tdfowler* - -* [DOCS] Apply more documentation for ActiveRecord Reflection. Closes #4055 *Robby Russell* - -* [DOCS] Document :allow_nil option of #validate_uniqueness_of. Closes #3143 *Caio Chassot* - -* Bring the sybase adapter up to scratch for 1.2 release. *jsheets* - -* Rollback new_record? and id when an exception is raised in a save callback. #6910 *Ben Curren, outerim* - -* Pushing a record on an association collection doesn't unnecessarily load all the associated records. *Obie Fernandez, Jeremy Kemper* - -* Oracle: fix connection reset failure. #6846 *leonlleslie* - -* Subclass instantiation doesn't try to explicitly require the corresponding subclass. #6840 *leei, Jeremy Kemper* - -* fix faulty inheritance tests and that eager loading grabs the wrong inheritance column when the class of your association is an STI subclass. Closes #6859 *protocool* - -* Consolidated different create and create! versions to call through to the base class with scope. This fixes inconsistencies, especially related to protected attribtues. Closes #5847 *Alexander Dymo, Tobias Lütke* - -* find supports :lock with :include. Check whether your database allows SELECT ... FOR UPDATE with outer joins before using. #6764 *vitaly, Jeremy Kemper* - -* Add AssociationCollection#create! to be consistent with AssociationCollection#create when dealing with a foreign key that is a protected attribute *Cody Fauser* - -* Added counter optimization for AssociationCollection#any? so person.friends.any? won't actually load the full association if we have the count in a cheaper form *David Heinemeier Hansson* - -* Change fixture_path to a class inheritable accessor allowing test cases to have their own custom set of fixtures. #6672 *Zach Dennis* - -* Quote ActiveSupport::Multibyte::Chars. #6653 *Julian Tarkhanov* - -* Simplify query_attribute by typecasting the attribute value and checking whether it's nil, false, zero or blank. #6659 *Jonathan Viney* - -* validates_numericality_of uses \A \Z to ensure the entire string matches rather than ^ $ which may match one valid line of a multiline string. #5716 *Andreas Schwarz* - -* Run validations in the order they were declared. #6657 *obrie* - -* MySQL: detect when a NOT NULL column without a default value is misreported as default ''. Can't detect for string, text, and binary columns since '' is a legitimate default. #6156 *simon@redhillconsulting.com.au, obrie, Jonathan Viney, Jeremy Kemper* - -* Simplify association proxy implementation by factoring construct_scope out of method_missing. #6643 *martin* - -* Oracle: automatically detect the primary key. #6594 *vesaria, Michael Schoen* - -* Oracle: to increase performance, prefetch 100 rows and enable similar cursor sharing. Both are configurable in database.yml. #6607 *philbogle@gmail.com, ray.fortna@jobster.com, Michael Schoen* - -* Don't inspect unloaded associations. #2905 *lmarlow* - -* SQLite: use AUTOINCREMENT primary key in >= 3.1.0. #6588, #6616 *careo, lukfugl* - -* Cache inheritance_column. #6592 *Stefan Kaes* - -* Firebird: decimal/numeric support. #6408 *macrnic* - -* make add_order a tad faster. #6567 *Stefan Kaes* - -* Find with :include respects scoped :order. #5850 - -* Support nil and Array in :conditions => { attr => value } hashes. #6548 *Assaf, Jeremy Kemper* - find(:all, :conditions => { :topic_id => [1, 2, 3], :last_read => nil } - -* Consistently use LOWER() for uniqueness validations (rather than mixing with UPPER()) so the database can always use a functional index on the lowercased column. #6495 *Si* - -* SQLite: fix calculations workaround, remove count(distinct) query rewrite, cleanup test connection scripts. *Jeremy Kemper* - -* SQLite: count(distinct) queries supported in >= 3.2.6. #6544 *Bob Silva* - -* Dynamically generate reader methods for serialized attributes. #6362 *Stefan Kaes* - -* Deprecation: object transactions warning. *Jeremy Kemper* - -* has_one :dependent => :nullify ignores nil associates. #4848, #6528 *bellis@deepthought.org, janovetz, Jeremy Kemper* - -* Oracle: resolve test failures, use prefetched primary key for inserts, check for null defaults, fix limited id selection for eager loading. Factor out some common methods from all adapters. #6515 *Michael Schoen* - -* Make add_column use the options hash with the Sqlite Adapter. Closes #6464 *obrie* - -* Document other options available to migration's add_column. #6419 *grg* - -* MySQL: all_hashes compatibility with old MysqlRes class. #6429, #6601 *Jeremy Kemper* - -* Fix has_many :through to add the appropriate conditions when going through an association using STI. Closes #5783. *Jonathan Viney* - -* fix select_limited_ids_list issues in postgresql, retain current behavior in other adapters *Rick Olson* - -* Restore eager condition interpolation, document it's differences *Rick Olson* - -* Don't rollback in teardown unless a transaction was started. Don't start a transaction in create_fixtures if a transaction is started. #6282 *Jacob Fugal, Jeremy Kemper* - -* Add #delete support to has_many :through associations. Closes #6049 *Martin Landers* - -* Reverted old select_limited_ids_list postgresql fix that caused issues in mysql. Closes #5851 *Rick Olson* - -* Removes the ability for eager loaded conditions to be interpolated, since there is no model instance to use as a context for interpolation. #5553 *turnip@turnipspatch.com* - -* Added timeout option to SQLite3 configurations to deal more gracefully with SQLite3::BusyException, now the connection can instead retry for x seconds to see if the db clears up before throwing that exception #6126 *wreese@gmail.com* - -* Added update_attributes! which uses save! to raise an exception if a validation error prevents saving #6192 *jonathan* - -* Deprecated add_on_boundary_breaking (use validates_length_of instead) #6292 *Bob Silva* - -* The has_many create method works with polymorphic associations. #6361 *Dan Peterson* - -* MySQL: introduce Mysql::Result#all_hashes to support further optimization. #5581 *Stefan Kaes* - -* save! shouldn't validate twice. #6324 *maiha, Bob Silva* - -* Association collections have an _ids reader method to match the existing writer for collection_select convenience (e.g. employee.task_ids). The writer method skips blank ids so you can safely do @employee.task_ids = params[:tasks] without checking every time for an empty list or blank values. #1887, #5780 *Michael Schuerig* - -* Add an attribute reader method for ActiveRecord::Base.observers *Rick Olson* - -* Deprecation: count class method should be called with an options hash rather than two args for conditions and joins. #6287 *Bob Silva* - -* has_one associations with a nil target may be safely marshaled. #6279 *norbauer, Jeremy Kemper* - -* Duplicate the hash provided to AR::Base#to_xml to prevent unexpected side effects *Michael Koziarski* - -* Add a :namespace option to AR::Base#to_xml *Michael Koziarski* - -* Deprecation tests. Remove warnings for dynamic finders and for the foo_count method if it's also an attribute. *Jeremy Kemper* - -* Mock Time.now for more accurate Touch mixin tests. #6213 *Dan Peterson* - -* Improve yaml fixtures error reporting. #6205 *Bruce Williams* - -* Rename AR::Base#quote so people can use that name in their models. #3628 *Michael Koziarski* - -* Add deprecation warning for inferred foreign key. #6029 *Josh Susser* - -* Fixed the Ruby/MySQL adapter we ship with Active Record to work with the new authentication handshake that was introduced in MySQL 4.1, along with the other protocol changes made at that time #5723 *jimw@mysql.com* - -* Deprecation: use :dependent => :delete_all rather than :exclusively_dependent => true. #6024 *Josh Susser* - -* Document validates_presences_of behavior with booleans: you probably want validates_inclusion_of :attr, :in => [true, false]. #2253 *Bob Silva* - -* Optimistic locking: gracefully handle nil versions, treat as zero. #5908 *Tom Ward* - -* to_xml: the :methods option works on arrays of records. #5845 *Josh Starcher* - -* Deprecation: update docs. #5998 *Jakob Skjerning, Kevin Clark* - -* Add some XmlSerialization tests for ActiveRecord *Rick Olson* - -* has_many :through conditions are sanitized by the associating class. #5971 *martin.emde@gmail.com* - -* Tighten rescue clauses. #5985 *james@grayproductions.net* - -* Fix spurious newlines and spaces in AR::Base#to_xml output *Jamis Buck* - -* has_one supports the :dependent => :delete option which skips the typical callback chain and deletes the associated object directly from the database. #5927 *Chris Mear, Jonathan Viney* - -* Nested subclasses are not prefixed with the parent class' table_name since they should always use the base class' table_name. #5911 *Jonathan Viney* - -* SQLServer: work around bug where some unambiguous date formats are not correctly identified if the session language is set to german. #5894 *Tom Ward, kruth@bfpi* - -* SQLServer: fix eager association test. #5901 *Tom Ward* - -* Clashing type columns due to a sloppy join shouldn't wreck single-table inheritance. #5838 *Kevin Clark* - -* Fixtures: correct escaping of \n and \r. #5859 *evgeny.zislis@gmail.com* - -* Migrations: gracefully handle missing migration files. #5857 *eli.gordon@gmail.com* - -* MySQL: update test schema for MySQL 5 strict mode. #5861 *Tom Ward* - -* to_xml: correct naming of included associations. #5831 *Josh Starcher* - -* Pushing a record onto a has_many :through sets the association's foreign key to the associate's primary key and adds it to the correct association. #5815, #5829 *Josh Susser* - -* Add records to has_many :through using <<, push, and concat by creating the association record. Raise if base or associate are new records since both ids are required to create the association. #build raises since you can't associate an unsaved record. #create! takes an attributes hash and creates the associated record and its association in a transaction. *Jeremy Kemper* - - # Create a tagging to associate the post and tag. - post.tags << Tag.find_by_name('old') - post.tags.create! :name => 'general' - - # Would have been: - post.taggings.create!(:tag => Tag.find_by_name('finally') - transaction do - post.taggings.create!(:tag => Tag.create!(:name => 'general')) - end - -* Cache nil results for :included has_one associations also. #5787 *Michael Schoen* - -* Fixed a bug which would cause .save to fail after trying to access a empty has_one association on a unsaved record. *Tobias Lütke* - -* Nested classes are given table names prefixed by the singular form of the parent's table name. *Jeremy Kemper* - Example: Invoice::Lineitem is given table name invoice_lineitems - -* Migrations: uniquely name multicolumn indexes so you don't have to. *Jeremy Kemper* - # people_active_last_name_index, people_active_deactivated_at_index - add_index :people, [:active, :last_name] - add_index :people, [:active, :deactivated_at] - remove_index :people, [:active, :last_name] - remove_index :people, [:active, :deactivated_at] - - WARNING: backward-incompatibility. Multicolumn indexes created before this - revision were named using the first column name only. Now they're uniquely - named using all indexed columns. - - To remove an old multicolumn index, remove_index :table_name, :first_column - -* Fix for deep includes on the same association. *richcollins@gmail.com* - -* Tweak fixtures so they don't try to use a non-ActiveRecord class. *Kevin Clark* - -* Remove ActiveRecord::Base.reset since Dispatcher doesn't use it anymore. *Rick Olson* - -* Document find's :from option. Closes #5762. *andrew@redlinesoftware.com* - -* PostgreSQL: autodetected sequences work correctly with multiple schemas. Rely on the schema search_path instead of explicitly qualifying the sequence name with its schema. #5280 *guy.naor@famundo.com* - -* Replace Reloadable with Reloadable::Deprecated. *Nicholas Seckar* - -* Cache nil results for has_one associations so multiple calls don't call the database. Closes #5757. *Michael Schoen* - -* Add documentation for how to disable timestamps on a per model basis. Closes #5684. *matt@mattmargolis.net Marcel Molina Jr.* - -* Don't save has_one associations unnecessarily. #5735 *Jonathan Viney* - -* Refactor ActiveRecord::Base.reset_subclasses to #reset, and add global observer resetting. *Rick Olson* - -* Formally deprecate the deprecated finders. *Michael Koziarski* - -* Formally deprecate rich associations. *Michael Koziarski* - -* Fixed that default timezones for new / initialize should uphold utc setting #5709 *daniluk@yahoo.com* - -* Fix announcement of very long migration names. #5722 *blake@near-time.com* - -* The exists? class method should treat a string argument as an id rather than as conditions. #5698 *jeremy@planetargon.com* - -* Fixed to_xml with :include misbehaviors when invoked on array of model instances #5690 *alexkwolfe@gmail.com* - -* Added support for conditions on Base.exists? #5689 [Josh Peek]. Examples: - - assert (Topic.exists?(:author_name => "David")) - assert (Topic.exists?(:author_name => "Mary", :approved => true)) - assert (Topic.exists?(["parent_id = ?", 1])) - -* Schema dumper quotes date :default values. *Dave Thomas* - -* Calculate sum with SQL, not Enumerable on HasManyThrough Associations. *Dan Peterson* - -* Factor the attribute#{suffix} methods out of method_missing for easier extension. *Jeremy Kemper* - -* Patch sql injection vulnerability when using integer or float columns. *Jamis Buck* - -* Allow #count through a has_many association to accept :include. *Dan Peterson* - -* create_table rdoc: suggest :id => false for habtm join tables. *Zed Shaw* - -* PostgreSQL: return array fields as strings. #4664 *Robby Russell* - -* SQLServer: added tests to ensure all database statements are closed, refactored identity_insert management code to use blocks, removed update/delete rowcount code out of execute and into update/delete, changed insert to go through execute method, removed unused quoting methods, disabled pessimistic locking tests as feature is currently unsupported, fixed RakeFile to load sqlserver specific tests whether running in ado or odbc mode, fixed support for recently added decimal types, added support for limits on integer types. #5670 *Tom Ward* - -* SQLServer: fix db:schema:dump case-sensitivity. #4684 *Will Rogers* - -* Oracle: BigDecimal support. #5667 *Michael Schoen* - -* Numeric and decimal columns map to BigDecimal instead of Float. Those with scale 0 map to Integer. #5454 *robbat2@gentoo.org, work@ashleymoran.me.uk* - -* Firebird migrations support. #5337 *Ken Kunz <kennethkunz@gmail.com>* - -* PostgreSQL: create/drop as postgres user. #4790 *mail@matthewpainter.co.uk, mlaster@metavillage.com* - -* Update callbacks documentation. #3970 *Robby Russell <robby@planetargon.com>* - -* PostgreSQL: correctly quote the ' in pk_and_sequence_for. #5462 *tietew@tietew.net* - -* PostgreSQL: correctly quote microseconds in timestamps. #5641 *rick@rickbradley.com* - -* Clearer has_one/belongs_to model names (account has_one :user). #5632 *matt@mattmargolis.net* - -* Oracle: use nonblocking queries if allow_concurrency is set, fix pessimistic locking, don't guess date vs. time by default (set OracleAdapter.emulate_dates = true for the old behavior), adapter cleanup. #5635 *Michael Schoen* - -* Fixed a few Oracle issues: Allows Oracle's odd date handling to still work consistently within #to_xml, Passes test that hardcode insert statement by dropping the :id column, Updated RUNNING_UNIT_TESTS with Oracle instructions, Corrects method signature for #exec #5294 *Michael Schoen* - -* Added :group to available options for finds done on associations #5516 *mike@michaeldewey.org* - -* Minor tweak to improve performance of ActiveRecord::Base#to_param. - -* Observers also watch subclasses created after they are declared. #5535 *daniels@pronto.com.au* - -* Removed deprecated timestamps_gmt class methods. *Jeremy Kemper* - -* rake build_mysql_database grants permissions to rails@localhost. #5501 *brianegge@yahoo.com* - -* PostgreSQL: support microsecond time resolution. #5492 *alex@msgpad.com* - -* Add AssociationCollection#sum since the method_missing invokation has been shadowed by Enumerable#sum. - -* Added find_or_initialize_by_X which works like find_or_create_by_X but doesn't save the newly instantiated record. *Sam Stephenson* - -* Row locking. Provide a locking clause with the :lock finder option or true for the default "FOR UPDATE". Use the #lock! method to obtain a row lock on a single record (reloads the record with :lock => true). *Shugo Maeda* - # Obtain an exclusive lock on person 1 so we can safely increment visits. - Person.transaction do - # select * from people where id=1 for update - person = Person.find(1, :lock => true) - person.visits += 1 - person.save! - end - -* PostgreSQL: introduce allow_concurrency option which determines whether to use blocking or asynchronous #execute. Adapters with blocking #execute will deadlock Ruby threads. The default value is ActiveRecord::Base.allow_concurrency. *Jeremy Kemper* - -* Use a per-thread (rather than global) transaction mutex so you may execute concurrent transactions on separate connections. *Jeremy Kemper* - -* Change AR::Base#to_param to return a String instead of a Fixnum. Closes #5320. *Nicholas Seckar* - -* Use explicit delegation instead of method aliasing for AR::Base.to_param -> AR::Base.id. #5299 (skaes@web.de) - -* Refactored ActiveRecord::Base.to_xml to become a delegate for XmlSerializer, which restores sanity to the mega method. This refactoring also reinstates the opinions that type="string" is redundant and ugly and nil-differentiation is not a concern of serialization *David Heinemeier Hansson* - -* Added simple hash conditions to find that'll just convert hash to an AND-based condition string #5143 [Hampton Catlin]. Example: - - Person.find(:all, :conditions => { :last_name => "Catlin", :status => 1 }, :limit => 2) - - ...is the same as: - Person.find(:all, :conditions => [ "last_name = ? and status = ?", "Catlin", 1 ], :limit => 2) - - This makes it easier to pass in the options from a form or otherwise outside. - - -* Fixed issues with BLOB limits, charsets, and booleans for Firebird #5194, #5191, #5189 *kennethkunz@gmail.com* - -* Fixed usage of :limit and with_scope when the association in scope is a 1:m #5208 *alex@purefiction.net* - -* Fixed migration trouble with SQLite when NOT NULL is used in the new definition #5215 *greg@lapcominc.com* - -* Fixed problems with eager loading and counting on SQL Server #5212 *kajism@yahoo.com* - -* Fixed that count distinct should use the selected column even when using :include #5251 *anna@wota.jp* - -* Fixed that :includes merged from with_scope won't cause the same association to be loaded more than once if repetition occurs in the clauses #5253 *alex@purefiction.net* - -* Allow models to override to_xml. #4989 *Blair Zajac <blair@orcaware.com>* - -* PostgreSQL: don't ignore port when host is nil since it's often used to label the domain socket. #5247 *shimbo@is.naist.jp* - -* Records and arrays of records are bound as quoted ids. *Jeremy Kemper* - Foo.find(:all, :conditions => ['bar_id IN (?)', bars]) - Foo.find(:first, :conditions => ['bar_id = ?', bar]) - -* Fixed that Base.find :all, :conditions => [ "id IN (?)", collection ] would fail if collection was empty *David Heinemeier Hansson* - -* Add a list of regexes assert_queries skips in the ActiveRecord test suite. *Rick Olson* - -* Fix the has_and_belongs_to_many #create doesn't populate the join for new records. Closes #3692 *Josh Susser* - -* Provide Association Extensions access to the instance that the association is being accessed from. - Closes #4433 *Josh Susser* - -* Update OpenBase adaterp's maintainer's email address. Closes #5176. *Derrick Spell* - -* Add a quick note about :select and eagerly included associations. *Rick Olson* - -* Add docs for the :as option in has_one associations. Closes #5144 *cdcarter@gmail.com* - -* Fixed that has_many collections shouldn't load the entire association to do build or create *David Heinemeier Hansson* - -* Added :allow_nil option for aggregations #5091 *Ian White* - -* Fix Oracle boolean support and tests. Closes #5139. *Michael Schoen* - -* create! no longer blows up when no attributes are passed and a :create scope is in effect (e.g. foo.bars.create! failed whereas foo.bars.create!({}) didn't.) *Jeremy Kemper* - -* Call Inflector#demodulize on the class name when eagerly including an STI model. Closes #5077 *info@loobmedia.com* - -* Preserve MySQL boolean column defaults when changing a column in a migration. Closes #5015. *pdcawley@bofh.org.uk* - -* PostgreSQL: migrations support :limit with :integer columns by mapping limit < 4 to smallint, > 4 to bigint, and anything else to integer. #2900 *keegan@thebasement.org* - -* Dates and times interpret empty strings as nil rather than 2000-01-01. #4830 *kajism@yahoo.com* - -* Allow :uniq => true with has_many :through associations. *Jeremy Kemper* - -* Ensure that StringIO is always available for the Schema dumper. *Marcel Molina Jr.* - -* Allow AR::Base#to_xml to include methods too. Closes #4921. *johan@textdrive.com* - -* Replace superfluous name_to_class_name variant with camelize. *Marcel Molina Jr.* - -* Replace alias method chaining with Module#alias_method_chain. *Marcel Molina Jr.* - -* Replace Ruby's deprecated append_features in favor of included. *Marcel Molina Jr.* - -* Remove duplicate fixture entry in comments.yml. Closes #4923. *Blair Zajac <blair@orcaware.com>* - -* Update FrontBase adapter to check binding version. Closes #4920. *mlaster@metavillage.com* - -* New Frontbase connections don't start in auto-commit mode. Closes #4922. *mlaster@metavillage.com* - -* When grouping, use the appropriate option key. *Marcel Molina Jr.* - -* Only modify the sequence name in the FrontBase adapter if the FrontBase adapter is actually being used. *Marcel Molina Jr.* - -* Add support for FrontBase (http://www.frontbase.com/) with a new adapter thanks to the hard work of one Mike Laster. Closes #4093. *mlaster@metavillage.com* - -* Add warning about the proper way to validate the presence of a foreign key. Closes #4147. *Francois Beausoleil <francois.beausoleil@gmail.com>* - -* Fix syntax error in documentation. Closes #4679. *Mislav Marohnić* - -* Add Oracle support for CLOB inserts. Closes #4748. *schoenm@earthlink.net sandra.metz@duke.edu* - -* Various fixes for sqlserver_adapter (odbc statement finishing, ado schema dumper, drop index). Closes #4831. *kajism@yahoo.com* - -* Add support for :order option to with_scope. Closes #3887. *eric.daspet@survol.net* - -* Prettify output of schema_dumper by making things line up. Closes #4241 *Caio Chassot <caio@v2studio.com>* - -* Make build_postgresql_databases task make databases owned by the postgres user. Closes #4790. *mlaster@metavillage.com* - -* Sybase Adapter type conversion cleanup. Closes #4736. *dev@metacasa.net* - -* Fix bug where calculations with long alias names return null. *Rick Olson* - -* Raise error when trying to add to a has_many :through association. Use the Join Model instead. *Rick Olson* - - @post.tags << @tag # BAD - @post.taggings.create(:tag => @tag) # GOOD - -* Allow all calculations to take the :include option, not just COUNT (closes #4840) *Rick Olson* - -* Update inconsistent migrations documentation. #4683 *machomagna@gmail.com* - -* Add ActiveRecord::Errors#to_xml *Jamis Buck* - -* Properly quote index names in migrations (closes #4764) *John Long* - -* Fix the HasManyAssociation#count method so it uses the new ActiveRecord::Base#count syntax, while maintaining backwards compatibility. *Rick Olson* - -* Ensure that Associations#include_eager_conditions? checks both scoped and explicit conditions *Rick Olson* - -* Associations#select_limited_ids_list adds the ORDER BY columns to the SELECT DISTINCT List for postgresql. *Rick Olson* - -* DRY up association collection reader method generation. *Marcel Molina Jr.* - -* DRY up and tweak style of the validation error object. *Marcel Molina Jr.* - -* Add :case_sensitive option to validates_uniqueness_of (closes #3090) *Rick Olson* - - class Account < ActiveRecord::Base - validates_uniqueness_of :email, :case_sensitive => false - end - -* Allow multiple association extensions with :extend option (closes #4666) *Josh Susser* - - class Account < ActiveRecord::Base - has_many :people, :extend => [FindOrCreateByNameExtension, FindRecentExtension] - end - - *1.15.3* (March 12th, 2007) - - * Allow a polymorphic :source for has_many :through associations. Closes #7143 [protocool] - - * Consistently quote primary key column names. #7763 [toolmantim] - - * Fixtures: fix YAML ordered map support. #2665 [Manuel Holtgrewe, nfbuckley] - - * Fix has_many :through << with custom foreign keys. #6466, #7153 [naffis, Rich Collins] - - -## 1.15.2 (February 5th, 2007) ## - -* Pass a range in :conditions to use the SQL BETWEEN operator. #6974 *Dan Manges* - Student.find(:all, :conditions => { :grade => 9..12 }) - -* Don't create instance writer methods for class attributes. *Rick Olson* - -* When dealing with SQLite3, use the table_info pragma helper, so that the bindings can do some translation for when sqlite3 breaks incompatibly between point releases. *Jamis Buck* - -* SQLServer: don't choke on strings containing 'null'. #7083 *Jakob Skjerning* - -* Consistently use LOWER() for uniqueness validations (rather than mixing with UPPER()) so the database can always use a functional index on the lowercased column. #6495 *Si* - -* MySQL: SET SQL_AUTO_IS_NULL=0 so 'where id is null' doesn't select the last inserted id. #6778 *Jonathan Viney, timc* - -* Fixtures use the table name and connection from set_fixture_class. #7330 *Anthony Eden* - -* SQLServer: quote table name in indexes query. #2928 *keithm@infused.org* - - -## 1.15.1 (January 17th, 2007) ## - -* Fix nodoc breaking of adapters - - -## 1.15.0 (January 16th, 2007) ## - -* [DOC] clear up some ambiguity with the way has_and_belongs_to_many creates the default join table name. #7072 *Jeremy McAnally* - -* change_column accepts :default => nil. Skip column options for primary keys. #6956, #7048 *Dan Manges, Jeremy Kemper* - -* MySQL, PostgreSQL: change_column_default quotes the default value and doesn't lose column type information. #3987, #6664 *Jonathan Viney, Manfred Stienstra, altano@bigfoot.com* - -* Oracle: create_table takes a :sequence_name option to override the 'tablename_seq' default. #7000 *Michael Schoen* - -* MySQL: retain SSL settings on reconnect. #6976 *randyv2* - -* SQLServer: handle [quoted] table names. #6635 *rrich* - -* acts_as_nested_set works with single-table inheritance. #6030 *Josh Susser* - -* PostgreSQL, Oracle: correctly perform eager finds with :limit and :order. #4668, #7021 *eventualbuddha, Michael Schoen* - -* Fix the Oracle adapter for serialized attributes stored in CLOBs. Closes #6825 *mschoen, tdfowler* - -* [DOCS] Apply more documentation for ActiveRecord Reflection. Closes #4055 *Robby Russell* - -* [DOCS] Document :allow_nil option of #validate_uniqueness_of. Closes #3143 *Caio Chassot* - -* Bring the sybase adapter up to scratch for 1.2 release. *jsheets* - -* Oracle: fix connection reset failure. #6846 *leonlleslie* - -* Subclass instantiation doesn't try to explicitly require the corresponding subclass. #6840 *leei, Jeremy Kemper* - -* fix faulty inheritance tests and that eager loading grabs the wrong inheritance column when the class of your association is an STI subclass. Closes #6859 *protocool* - -* find supports :lock with :include. Check whether your database allows SELECT ... FOR UPDATE with outer joins before using. #6764 *vitaly, Jeremy Kemper* - -* Support nil and Array in :conditions => { attr => value } hashes. #6548 *Assaf, Jeremy Kemper* - find(:all, :conditions => { :topic_id => [1, 2, 3], :last_read => nil } - -* Quote ActiveSupport::Multibyte::Chars. #6653 *Julian Tarkhanov* - -* MySQL: detect when a NOT NULL column without a default value is misreported as default ''. Can't detect for string, text, and binary columns since '' is a legitimate default. #6156 *simon@redhillconsulting.com.au, obrie, Jonathan Viney, Jeremy Kemper* - -* validates_numericality_of uses \A \Z to ensure the entire string matches rather than ^ $ which may match one valid line of a multiline string. #5716 *Andreas Schwarz* - -* Oracle: automatically detect the primary key. #6594 *vesaria, Michael Schoen* - -* Oracle: to increase performance, prefetch 100 rows and enable similar cursor sharing. Both are configurable in database.yml. #6607 *philbogle@gmail.com, ray.fortna@jobster.com, Michael Schoen* - -* Firebird: decimal/numeric support. #6408 *macrnic* - -* Find with :include respects scoped :order. #5850 - -* Dynamically generate reader methods for serialized attributes. #6362 *Stefan Kaes* - -* Deprecation: object transactions warning. *Jeremy Kemper* - -* has_one :dependent => :nullify ignores nil associates. #6528 *janovetz, Jeremy Kemper* - -* Oracle: resolve test failures, use prefetched primary key for inserts, check for null defaults, fix limited id selection for eager loading. Factor out some common methods from all adapters. #6515 *Michael Schoen* - -* Make add_column use the options hash with the Sqlite Adapter. Closes #6464 *obrie* - -* Document other options available to migration's add_column. #6419 *grg* - -* MySQL: all_hashes compatibility with old MysqlRes class. #6429, #6601 *Jeremy Kemper* - -* Fix has_many :through to add the appropriate conditions when going through an association using STI. Closes #5783. *Jonathan Viney* - -* fix select_limited_ids_list issues in postgresql, retain current behavior in other adapters *Rick Olson* - -* Restore eager condition interpolation, document it's differences *Rick Olson* - -* Don't rollback in teardown unless a transaction was started. Don't start a transaction in create_fixtures if a transaction is started. #6282 *Jacob Fugal, Jeremy Kemper* - -* Add #delete support to has_many :through associations. Closes #6049 *Martin Landers* - -* Reverted old select_limited_ids_list postgresql fix that caused issues in mysql. Closes #5851 *Rick Olson* - -* Removes the ability for eager loaded conditions to be interpolated, since there is no model instance to use as a context for interpolation. #5553 *turnip@turnipspatch.com* - -* Added timeout option to SQLite3 configurations to deal more gracefully with SQLite3::BusyException, now the connection can instead retry for x seconds to see if the db clears up before throwing that exception #6126 *wreese@gmail.com* - -* Added update_attributes! which uses save! to raise an exception if a validation error prevents saving #6192 *jonathan* - -* Deprecated add_on_boundary_breaking (use validates_length_of instead) #6292 *Bob Silva* - -* The has_many create method works with polymorphic associations. #6361 *Dan Peterson* - -* MySQL: introduce Mysql::Result#all_hashes to support further optimization. #5581 *Stefan Kaes* - -* save! shouldn't validate twice. #6324 *maiha, Bob Silva* - -* Association collections have an _ids reader method to match the existing writer for collection_select convenience (e.g. employee.task_ids). The writer method skips blank ids so you can safely do @employee.task_ids = params[:tasks] without checking every time for an empty list or blank values. #1887, #5780 *Michael Schuerig* - -* Add an attribute reader method for ActiveRecord::Base.observers *Rick Olson* - -* Deprecation: count class method should be called with an options hash rather than two args for conditions and joins. #6287 *Bob Silva* - -* has_one associations with a nil target may be safely marshaled. #6279 *norbauer, Jeremy Kemper* - -* Duplicate the hash provided to AR::Base#to_xml to prevent unexpected side effects *Michael Koziarski* - -* Add a :namespace option to AR::Base#to_xml *Michael Koziarski* - -* Deprecation tests. Remove warnings for dynamic finders and for the foo_count method if it's also an attribute. *Jeremy Kemper* - -* Mock Time.now for more accurate Touch mixin tests. #6213 *Dan Peterson* - -* Improve yaml fixtures error reporting. #6205 *Bruce Williams* - -* Rename AR::Base#quote so people can use that name in their models. #3628 *Michael Koziarski* - -* Add deprecation warning for inferred foreign key. #6029 *Josh Susser* - -* Fixed the Ruby/MySQL adapter we ship with Active Record to work with the new authentication handshake that was introduced in MySQL 4.1, along with the other protocol changes made at that time #5723 *jimw@mysql.com* - -* Deprecation: use :dependent => :delete_all rather than :exclusively_dependent => true. #6024 *Josh Susser* - -* Optimistic locking: gracefully handle nil versions, treat as zero. #5908 *Tom Ward* - -* to_xml: the :methods option works on arrays of records. #5845 *Josh Starcher* - -* has_many :through conditions are sanitized by the associating class. #5971 *martin.emde@gmail.com* - -* Fix spurious newlines and spaces in AR::Base#to_xml output *Jamis Buck* - -* has_one supports the :dependent => :delete option which skips the typical callback chain and deletes the associated object directly from the database. #5927 *Chris Mear, Jonathan Viney* - -* Nested subclasses are not prefixed with the parent class' table_name since they should always use the base class' table_name. #5911 *Jonathan Viney* - -* SQLServer: work around bug where some unambiguous date formats are not correctly identified if the session language is set to german. #5894 *Tom Ward, kruth@bfpi* - -* Clashing type columns due to a sloppy join shouldn't wreck single-table inheritance. #5838 *Kevin Clark* - -* Fixtures: correct escaping of \n and \r. #5859 *evgeny.zislis@gmail.com* - -* Migrations: gracefully handle missing migration files. #5857 *eli.gordon@gmail.com* - -* MySQL: update test schema for MySQL 5 strict mode. #5861 *Tom Ward* - -* to_xml: correct naming of included associations. #5831 *Josh Starcher* - -* Pushing a record onto a has_many :through sets the association's foreign key to the associate's primary key and adds it to the correct association. #5815, #5829 *Josh Susser* - -* Add records to has_many :through using <<, push, and concat by creating the association record. Raise if base or associate are new records since both ids are required to create the association. #build raises since you can't associate an unsaved record. #create! takes an attributes hash and creates the associated record and its association in a transaction. *Jeremy Kemper* - - # Create a tagging to associate the post and tag. - post.tags << Tag.find_by_name('old') - post.tags.create! :name => 'general' - - # Would have been: - post.taggings.create!(:tag => Tag.find_by_name('finally') - transaction do - post.taggings.create!(:tag => Tag.create!(:name => 'general')) - end - -* Cache nil results for :included has_one associations also. #5787 *Michael Schoen* - -* Fixed a bug which would cause .save to fail after trying to access a empty has_one association on a unsaved record. *Tobias Lütke* - -* Nested classes are given table names prefixed by the singular form of the parent's table name. *Jeremy Kemper* - Example: Invoice::Lineitem is given table name invoice_lineitems - -* Migrations: uniquely name multicolumn indexes so you don't have to. *Jeremy Kemper* - # people_active_last_name_index, people_active_deactivated_at_index - add_index :people, [:active, :last_name] - add_index :people, [:active, :deactivated_at] - remove_index :people, [:active, :last_name] - remove_index :people, [:active, :deactivated_at] - - WARNING: backward-incompatibility. Multicolumn indexes created before this - revision were named using the first column name only. Now they're uniquely - named using all indexed columns. - - To remove an old multicolumn index, remove_index :table_name, :first_column - -* Fix for deep includes on the same association. *richcollins@gmail.com* - -* Tweak fixtures so they don't try to use a non-ActiveRecord class. *Kevin Clark* - -* Remove ActiveRecord::Base.reset since Dispatcher doesn't use it anymore. *Rick Olson* - -* PostgreSQL: autodetected sequences work correctly with multiple schemas. Rely on the schema search_path instead of explicitly qualifying the sequence name with its schema. #5280 *guy.naor@famundo.com* - -* Replace Reloadable with Reloadable::Deprecated. *Nicholas Seckar* - -* Cache nil results for has_one associations so multiple calls don't call the database. Closes #5757. *Michael Schoen* - -* Don't save has_one associations unnecessarily. #5735 *Jonathan Viney* - -* Refactor ActiveRecord::Base.reset_subclasses to #reset, and add global observer resetting. *Rick Olson* - -* Formally deprecate the deprecated finders. *Michael Koziarski* - -* Formally deprecate rich associations. *Michael Koziarski* - -* Fixed that default timezones for new / initialize should uphold utc setting #5709 *daniluk@yahoo.com* - -* Fix announcement of very long migration names. #5722 *blake@near-time.com* - -* The exists? class method should treat a string argument as an id rather than as conditions. #5698 *jeremy@planetargon.com* - -* Fixed to_xml with :include misbehaviors when invoked on array of model instances #5690 *alexkwolfe@gmail.com* - -* Added support for conditions on Base.exists? #5689 [Josh Peek]. Examples: - - assert (Topic.exists?(:author_name => "David")) - assert (Topic.exists?(:author_name => "Mary", :approved => true)) - assert (Topic.exists?(["parent_id = ?", 1])) - -* Schema dumper quotes date :default values. *Dave Thomas* - -* Calculate sum with SQL, not Enumerable on HasManyThrough Associations. *Dan Peterson* - -* Factor the attribute#{suffix} methods out of method_missing for easier extension. *Jeremy Kemper* - -* Patch sql injection vulnerability when using integer or float columns. *Jamis Buck* - -* Allow #count through a has_many association to accept :include. *Dan Peterson* - -* create_table rdoc: suggest :id => false for habtm join tables. *Zed Shaw* - -* PostgreSQL: return array fields as strings. #4664 *Robby Russell* - -* SQLServer: added tests to ensure all database statements are closed, refactored identity_insert management code to use blocks, removed update/delete rowcount code out of execute and into update/delete, changed insert to go through execute method, removed unused quoting methods, disabled pessimistic locking tests as feature is currently unsupported, fixed RakeFile to load sqlserver specific tests whether running in ado or odbc mode, fixed support for recently added decimal types, added support for limits on integer types. #5670 *Tom Ward* - -* SQLServer: fix db:schema:dump case-sensitivity. #4684 *Will Rogers* - -* Oracle: BigDecimal support. #5667 *Michael Schoen* - -* Numeric and decimal columns map to BigDecimal instead of Float. Those with scale 0 map to Integer. #5454 *robbat2@gentoo.org, work@ashleymoran.me.uk* - -* Firebird migrations support. #5337 *Ken Kunz <kennethkunz@gmail.com>* - -* PostgreSQL: create/drop as postgres user. #4790 *mail@matthewpainter.co.uk, mlaster@metavillage.com* - -* PostgreSQL: correctly quote the ' in pk_and_sequence_for. #5462 *tietew@tietew.net* - -* PostgreSQL: correctly quote microseconds in timestamps. #5641 *rick@rickbradley.com* - -* Clearer has_one/belongs_to model names (account has_one :user). #5632 *matt@mattmargolis.net* - -* Oracle: use nonblocking queries if allow_concurrency is set, fix pessimistic locking, don't guess date vs. time by default (set OracleAdapter.emulate_dates = true for the old behavior), adapter cleanup. #5635 *Michael Schoen* - -* Fixed a few Oracle issues: Allows Oracle's odd date handling to still work consistently within #to_xml, Passes test that hardcode insert statement by dropping the :id column, Updated RUNNING_UNIT_TESTS with Oracle instructions, Corrects method signature for #exec #5294 *Michael Schoen* - -* Added :group to available options for finds done on associations #5516 *mike@michaeldewey.org* - -* Observers also watch subclasses created after they are declared. #5535 *daniels@pronto.com.au* - -* Removed deprecated timestamps_gmt class methods. *Jeremy Kemper* - -* rake build_mysql_database grants permissions to rails@localhost. #5501 *brianegge@yahoo.com* - -* PostgreSQL: support microsecond time resolution. #5492 *alex@msgpad.com* - -* Add AssociationCollection#sum since the method_missing invokation has been shadowed by Enumerable#sum. - -* Added find_or_initialize_by_X which works like find_or_create_by_X but doesn't save the newly instantiated record. *Sam Stephenson* - -* Row locking. Provide a locking clause with the :lock finder option or true for the default "FOR UPDATE". Use the #lock! method to obtain a row lock on a single record (reloads the record with :lock => true). *Shugo Maeda* - # Obtain an exclusive lock on person 1 so we can safely increment visits. - Person.transaction do - # select * from people where id=1 for update - person = Person.find(1, :lock => true) - person.visits += 1 - person.save! - end - -* PostgreSQL: introduce allow_concurrency option which determines whether to use blocking or asynchronous #execute. Adapters with blocking #execute will deadlock Ruby threads. The default value is ActiveRecord::Base.allow_concurrency. *Jeremy Kemper* - -* Use a per-thread (rather than global) transaction mutex so you may execute concurrent transactions on separate connections. *Jeremy Kemper* - -* Change AR::Base#to_param to return a String instead of a Fixnum. Closes #5320. *Nicholas Seckar* - -* Use explicit delegation instead of method aliasing for AR::Base.to_param -> AR::Base.id. #5299 (skaes@web.de) - -* Refactored ActiveRecord::Base.to_xml to become a delegate for XmlSerializer, which restores sanity to the mega method. This refactoring also reinstates the opinions that type="string" is redundant and ugly and nil-differentiation is not a concern of serialization *David Heinemeier Hansson* - -* Added simple hash conditions to find that'll just convert hash to an AND-based condition string #5143 [Hampton Catlin]. Example: - - Person.find(:all, :conditions => { :last_name => "Catlin", :status => 1 }, :limit => 2) - - ...is the same as: - Person.find(:all, :conditions => [ "last_name = ? and status = ?", "Catlin", 1 ], :limit => 2) - - This makes it easier to pass in the options from a form or otherwise outside. - - -* Fixed issues with BLOB limits, charsets, and booleans for Firebird #5194, #5191, #5189 *kennethkunz@gmail.com* - -* Fixed usage of :limit and with_scope when the association in scope is a 1:m #5208 *alex@purefiction.net* - -* Fixed migration trouble with SQLite when NOT NULL is used in the new definition #5215 *greg@lapcominc.com* - -* Fixed problems with eager loading and counting on SQL Server #5212 *kajism@yahoo.com* - -* Fixed that count distinct should use the selected column even when using :include #5251 *anna@wota.jp* - -* Fixed that :includes merged from with_scope won't cause the same association to be loaded more than once if repetition occurs in the clauses #5253 *alex@purefiction.net* - -* Allow models to override to_xml. #4989 *Blair Zajac <blair@orcaware.com>* - -* PostgreSQL: don't ignore port when host is nil since it's often used to label the domain socket. #5247 *shimbo@is.naist.jp* - -* Records and arrays of records are bound as quoted ids. *Jeremy Kemper* - Foo.find(:all, :conditions => ['bar_id IN (?)', bars]) - Foo.find(:first, :conditions => ['bar_id = ?', bar]) - -* Fixed that Base.find :all, :conditions => [ "id IN (?)", collection ] would fail if collection was empty *David Heinemeier Hansson* - -* Add a list of regexes assert_queries skips in the ActiveRecord test suite. *Rick Olson* - -* Fix the has_and_belongs_to_many #create doesn't populate the join for new records. Closes #3692 *Josh Susser* - -* Provide Association Extensions access to the instance that the association is being accessed from. - Closes #4433 *Josh Susser* - -* Update OpenBase adaterp's maintainer's email address. Closes #5176. *Derrick Spell* - -* Add a quick note about :select and eagerly included associations. *Rick Olson* - -* Add docs for the :as option in has_one associations. Closes #5144 *cdcarter@gmail.com* - -* Fixed that has_many collections shouldn't load the entire association to do build or create *David Heinemeier Hansson* - -* Added :allow_nil option for aggregations #5091 *Ian White* - -* Fix Oracle boolean support and tests. Closes #5139. *Michael Schoen* - -* create! no longer blows up when no attributes are passed and a :create scope is in effect (e.g. foo.bars.create! failed whereas foo.bars.create!({}) didn't.) *Jeremy Kemper* - -* Call Inflector#demodulize on the class name when eagerly including an STI model. Closes #5077 *info@loobmedia.com* - -* Preserve MySQL boolean column defaults when changing a column in a migration. Closes #5015. *pdcawley@bofh.org.uk* - -* PostgreSQL: migrations support :limit with :integer columns by mapping limit < 4 to smallint, > 4 to bigint, and anything else to integer. #2900 *keegan@thebasement.org* - -* Dates and times interpret empty strings as nil rather than 2000-01-01. #4830 *kajism@yahoo.com* - -* Allow :uniq => true with has_many :through associations. *Jeremy Kemper* - -* Ensure that StringIO is always available for the Schema dumper. *Marcel Molina Jr.* - -* Allow AR::Base#to_xml to include methods too. Closes #4921. *johan@textdrive.com* - -* Remove duplicate fixture entry in comments.yml. Closes #4923. *Blair Zajac <blair@orcaware.com>* - -* When grouping, use the appropriate option key. *Marcel Molina Jr.* - -* Add support for FrontBase (http://www.frontbase.com/) with a new adapter thanks to the hard work of one Mike Laster. Closes #4093. *mlaster@metavillage.com* - -* Add warning about the proper way to validate the presence of a foreign key. Closes #4147. *Francois Beausoleil <francois.beausoleil@gmail.com>* - -* Fix syntax error in documentation. Closes #4679. *Mislav Marohnić* - -* Add Oracle support for CLOB inserts. Closes #4748. *schoenm@earthlink.net sandra.metz@duke.edu* - -* Various fixes for sqlserver_adapter (odbc statement finishing, ado schema dumper, drop index). Closes #4831. *kajism@yahoo.com* - -* Add support for :order option to with_scope. Closes #3887. *eric.daspet@survol.net* - -* Prettify output of schema_dumper by making things line up. Closes #4241 *Caio Chassot <caio@v2studio.com>* - -* Make build_postgresql_databases task make databases owned by the postgres user. Closes #4790. *mlaster@metavillage.com* - -* Sybase Adapter type conversion cleanup. Closes #4736. *dev@metacasa.net* - -* Fix bug where calculations with long alias names return null. *Rick Olson* - -* Raise error when trying to add to a has_many :through association. Use the Join Model instead. *Rick Olson* - - @post.tags << @tag # BAD - @post.taggings.create(:tag => @tag) # GOOD - -* Allow all calculations to take the :include option, not just COUNT (closes #4840) *Rick Olson* - -* Add ActiveRecord::Errors#to_xml *Jamis Buck* - -* Properly quote index names in migrations (closes #4764) *John Long* - -* Fix the HasManyAssociation#count method so it uses the new ActiveRecord::Base#count syntax, while maintaining backwards compatibility. *Rick Olson* - -* Ensure that Associations#include_eager_conditions? checks both scoped and explicit conditions *Rick Olson* - -* Associations#select_limited_ids_list adds the ORDER BY columns to the SELECT DISTINCT List for postgresql. *Rick Olson* - -* Add :case_sensitive option to validates_uniqueness_of (closes #3090) *Rick Olson* - - class Account < ActiveRecord::Base - validates_uniqueness_of :email, :case_sensitive => false - end - -* Allow multiple association extensions with :extend option (closes #4666) *Josh Susser* - - class Account < ActiveRecord::Base - has_many :people, :extend => [FindOrCreateByNameExtension, FindRecentExtension] - end - - -## 1.14.4 (August 8th, 2006) ## - -* Add warning about the proper way to validate the presence of a foreign key. #4147 *Francois Beausoleil <francois.beausoleil@gmail.com>* - -* Fix syntax error in documentation. #4679 *Mislav Marohnić* - -* Update inconsistent migrations documentation. #4683 *machomagna@gmail.com* - - -## 1.14.3 (June 27th, 2006) ## - -* Fix announcement of very long migration names. #5722 *blake@near-time.com* - -* Update callbacks documentation. #3970 *Robby Russell <robby@planetargon.com>* - -* Properly quote index names in migrations (closes #4764) *John Long* - -* Ensure that Associations#include_eager_conditions? checks both scoped and explicit conditions *Rick Olson* - -* Associations#select_limited_ids_list adds the ORDER BY columns to the SELECT DISTINCT List for postgresql. *Rick Olson* - - -## 1.14.2 (April 9th, 2006) ## - -* Fixed calculations for the Oracle Adapter (closes #4626) *Michael Schoen* - - -## 1.14.1 (April 6th, 2006) ## - -* Fix type_name_with_module to handle type names that begin with '::'. Closes #4614. *Nicholas Seckar* - -* Fixed that that multiparameter assignment doesn't work with aggregations (closes #4620) *Lars Pind* - -* Enable Limit/Offset in Calculations (closes #4558) *lmarlow* - -* Fixed that loading including associations returns all results if Load IDs For Limited Eager Loading returns none (closes #4528) *Rick Olson* - -* Fixed HasManyAssociation#find bugs when :finder_sql is set #4600 *lagroue@free.fr* - -* Allow AR::Base#respond_to? to behave when @attributes is nil *Ryan Davis* - -* Support eager includes when going through a polymorphic has_many association. *Rick Olson* - -* Added support for eagerly including polymorphic has_one associations. (closes #4525) *Rick Olson* - - class Post < ActiveRecord::Base - has_one :tagging, :as => :taggable - end - - Post.find :all, :include => :tagging - -* Added descriptive error messages for invalid has_many :through associations: going through :has_one or :has_and_belongs_to_many *Rick Olson* - -* Added support for going through a polymorphic has_many association: (closes #4401) *Rick Olson* - - class PhotoCollection < ActiveRecord::Base - has_many :photos, :as => :photographic - belongs_to :firm - end - - class Firm < ActiveRecord::Base - has_many :photo_collections - has_many :photos, :through => :photo_collections - end - -* Multiple fixes and optimizations in PostgreSQL adapter, allowing ruby-postgres gem to work properly. *ruben.nine@gmail.com* - -* Fixed that AssociationCollection#delete_all should work even if the records of the association are not loaded yet. *Florian Weber* - -* Changed those private ActiveRecord methods to take optional third argument :auto instead of nil for performance optimizations. (closes #4456) *Stefan* - -* Private ActiveRecord methods add_limit!, add_joins!, and add_conditions! take an OPTIONAL third argument 'scope' (closes #4456) *Rick Olson* - -* DEPRECATED: Using additional attributes on has_and_belongs_to_many associations. Instead upgrade your association to be a real join model *David Heinemeier Hansson* - -* Fixed that records returned from has_and_belongs_to_many associations with additional attributes should be marked as read only (fixes #4512) *David Heinemeier Hansson* - -* Do not implicitly mark recordss of has_many :through as readonly but do mark habtm records as readonly (eventually only on join tables without rich attributes). *Marcel Mollina Jr.* - -* Fixed broken OCIAdapter #4457 *Michael Schoen* - - -## 1.14.0 (March 27th, 2006) ## - -* Replace 'rescue Object' with a finer grained rescue. Closes #4431. *Nicholas Seckar* - -* Fixed eager loading so that an aliased table cannot clash with a has_and_belongs_to_many join table *Rick Olson* - -* Add support for :include to with_scope *andrew@redlinesoftware.com* - -* Support the use of public synonyms with the Oracle adapter; required ruby-oci8 v0.1.14 #4390 *Michael Schoen* - -* Change periods (.) in table aliases to _'s. Closes #4251 *jeff@ministrycentered.com* - -* Changed has_and_belongs_to_many join to INNER JOIN for Mysql 3.23.x. Closes #4348 *Rick Olson* - -* Fixed issue that kept :select options from being scoped *Rick Olson* - -* Fixed db_schema_import when binary types are present #3101 *David Heinemeier Hansson* - -* Fixed that MySQL enums should always be returned as strings #3501 *David Heinemeier Hansson* - -* Change has_many :through to use the :source option to specify the source association. :class_name is now ignored. *Rick Olson* - - class Connection < ActiveRecord::Base - belongs_to :user - belongs_to :channel - end - - class Channel < ActiveRecord::Base - has_many :connections - has_many :contacts, :through => :connections, :class_name => 'User' # OLD - has_many :contacts, :through => :connections, :source => :user # NEW - end - -* Fixed DB2 adapter so nullable columns will be determines correctly now and quotes from column default values will be removed #4350 *contact@maik-schmidt.de* - -* Allow overriding of find parameters in scoped has_many :through calls *Rick Olson* - - In this example, :include => false disables the default eager association from loading. :select changes the standard - select clause. :joins specifies a join that is added to the end of the has_many :through query. - - class Post < ActiveRecord::Base - has_many :tags, :through => :taggings, :include => :tagging do - def add_joins_and_select - find :all, :select => 'tags.*, authors.id as author_id', :include => false, - :joins => 'left outer join posts on taggings.taggable_id = posts.id left outer join authors on posts.author_id = authors.id' - end - end - end - -* Fixed that schema changes while the database was open would break any connections to an SQLite database (now we reconnect if that error is throw) *David Heinemeier Hansson* - -* Don't classify the has_one class when eager loading, it is already singular. Add tests. (closes #4117) *Jonathan Viney* - -* Quit ignoring default :include options in has_many :through calls *Mark James* - -* Allow has_many :through associations to find the source association by setting a custom class (closes #4307) *Jonathan Viney* - -* Eager Loading support added for has_many :through => :has_many associations (see below). *Rick Olson* - -* Allow has_many :through to work on has_many associations (closes #3864) [sco@scottraymond.net] Example: - - class Firm < ActiveRecord::Base - has_many :clients - has_many :invoices, :through => :clients - end - - class Client < ActiveRecord::Base - belongs_to :firm - has_many :invoices - end - - class Invoice < ActiveRecord::Base - belongs_to :client - end - -* Raise error when trying to select many polymorphic objects with has_many :through or :include (closes #4226) *Josh Susser* - -* Fixed has_many :through to include :conditions set on the :through association. closes #4020 *Jonathan Viney* - -* Fix that has_many :through honors the foreign key set by the belongs_to association in the join model (closes #4259) *andylien@gmail.com / Rick Olson* - -* SQL Server adapter gets some love #4298 *Ryan Tomayko* - -* Added OpenBase database adapter that builds on top of the http://www.spice-of-life.net/ruby-openbase/ driver. All functionality except LIMIT/OFFSET is supported #3528 *derrickspell@cdmplus.com* - -* Rework table aliasing to account for truncated table aliases. Add smarter table aliasing when doing eager loading of STI associations. This allows you to use the association name in the order/where clause. [Jonathan Viney / Rick Olson] #4108 Example (SpecialComment is using STI): - - Author.find(:all, :include => { :posts => :special_comments }, :order => 'special_comments.body') - -* Add AbstractAdapter#table_alias_for to create table aliases according to the rules of the current adapter. *Rick Olson* - -* Provide access to the underlying database connection through Adapter#raw_connection. Enables the use of db-specific methods without complicating the adapters. #2090 *Michael Koziarski* - -* Remove broken attempts at handling columns with a default of 'now()' in the postgresql adapter. #2257 *Michael Koziarski* - -* Added connection#current_database that'll return of the current database (only works in MySQL, SQL Server, and Oracle so far -- please help implement for the rest of the adapters) #3663 *Tom Ward* - -* Fixed that Migration#execute would have the table name prefix appended to its query #4110 *mark.imbriaco@pobox.com* - -* Make all tinyint(1) variants act like boolean in mysql (tinyint(1) unsigned, etc.) *Jamis Buck* - -* Use association's :conditions when eager loading. [Jeremy Evans] #4144 - -* Alias the has_and_belongs_to_many join table on eager includes. #4106 *Jeremy Evans* - - This statement would normally error because the projects_developers table is joined twice, and therefore joined_on would be ambiguous. - - Developer.find(:all, :include => {:projects => :developers}, :conditions => 'join_project_developers.joined_on IS NOT NULL') - -* Oracle adapter gets some love #4230 *Michael Schoen* - - * Changes :text to CLOB rather than BLOB [Moses Hohman] - * Fixes an issue with nil numeric length/scales (several) - * Implements support for XMLTYPE columns [wilig / Kubo Takehiro] - * Tweaks a unit test to get it all green again - * Adds support for #current_database - -* Added Base.abstract_class? that marks which classes are not part of the Active Record hierarchy #3704 *Rick Olson* - - class CachedModel < ActiveRecord::Base - self.abstract_class = true - end - - class Post < CachedModel - end - - CachedModel.abstract_class? - => true - - Post.abstract_class? - => false - - Post.base_class - => Post - - Post.table_name - => 'posts' - -* Allow :dependent options to be used with polymorphic joins. #3820 *Rick Olson* - - class Foo < ActiveRecord::Base - has_many :attachments, :as => :attachable, :dependent => :delete_all - end - -* Nicer error message on has_many :through when :through reflection can not be found. #4042 *court3nay* - -* Upgrade to Transaction::Simple 1.3 *Jamis Buck* - -* Catch FixtureClassNotFound when using instantiated fixtures on a fixture that has no ActiveRecord model *Rick Olson* - -* Allow ordering of calculated results and/or grouped fields in calculations *solo@gatelys.com* - -* Make ActiveRecord::Base#save! return true instead of nil on success. #4173 *johan@johansorensen.com* - -* Dynamically set allow_concurrency. #4044 *Stefan Kaes* - -* Added Base#to_xml that'll turn the current record into a XML representation [David Heinemeier Hansson]. Example: - - topic.to_xml - - ...returns: - - <?xml version="1.0" encoding="UTF-8"?> - <topic> - <title>The First Topic</title> - <author-name>David</author-name> - <id type="integer">1</id> - <approved type="boolean">false</approved> - <replies-count type="integer">0</replies-count> - <bonus-time type="datetime">2000-01-01 08:28:00</bonus-time> - <written-on type="datetime">2003-07-16 09:28:00</written-on> - <content>Have a nice day</content> - <author-email-address>david@loudthinking.com</author-email-address> - <parent-id></parent-id> - <last-read type="date">2004-04-15</last-read> - </topic> - - ...and you can configure with: - - topic.to_xml(:skip_instruct => true, :except => [ :id, bonus_time, :written_on, replies_count ]) - - ...that'll return: - - <topic> - <title>The First Topic</title> - <author-name>David</author-name> - <approved type="boolean">false</approved> - <content>Have a nice day</content> - <author-email-address>david@loudthinking.com</author-email-address> - <parent-id></parent-id> - <last-read type="date">2004-04-15</last-read> - </topic> - - You can even do load first-level associations as part of the document: - - firm.to_xml :include => [ :account, :clients ] - - ...that'll return something like: - - <?xml version="1.0" encoding="UTF-8"?> - <firm> - <id type="integer">1</id> - <rating type="integer">1</rating> - <name>37signals</name> - <clients> - <client> - <rating type="integer">1</rating> - <name>Summit</name> - </client> - <client> - <rating type="integer">1</rating> - <name>Microsoft</name> - </client> - </clients> - <account> - <id type="integer">1</id> - <credit-limit type="integer">50</credit-limit> - </account> - </firm> - -* Allow :counter_cache to take a column name for custom counter cache columns *Jamis Buck* - -* Documentation fixes for :dependent *robby@planetargon.com* - -* Stop the MySQL adapter crashing when views are present. #3782 *Jonathan Viney* - -* Don't classify the belongs_to class, it is already singular #4117 *keithm@infused.org* - -* Allow set_fixture_class to take Classes instead of strings for a class in a module. Raise FixtureClassNotFound if a fixture can't load. *Rick Olson* - -* Fix quoting of inheritance column for STI eager loading #4098 *Jonathan Viney <jonathan@bluewire.net.nz>* - -* Added smarter table aliasing for eager associations for multiple self joins #3580 *Rick Olson* - - * The first time a table is referenced in a join, no alias is used. - * After that, the parent class name and the reflection name are used. - - Tree.find(:all, :include => :children) # LEFT OUTER JOIN trees AS tree_children ... - - * Any additional join references get a numerical suffix like '_2', '_3', etc. - -* Fixed eager loading problems with single-table inheritance #3580 [Rick Olson]. Post.find(:all, :include => :special_comments) now returns all posts, and any special comments that the posts may have. And made STI work with has_many :through and polymorphic belongs_to. - -* Added cascading eager loading that allows for queries like Author.find(:all, :include=> { :posts=> :comments }), which will fetch all authors, their posts, and the comments belonging to those posts in a single query (using LEFT OUTER JOIN) #3913 [anna@wota.jp]. Examples: - - # cascaded in two levels - >> Author.find(:all, :include=>{:posts=>:comments}) - => authors - +- posts - +- comments - - # cascaded in two levels and normal association - >> Author.find(:all, :include=>[{:posts=>:comments}, :categorizations]) - => authors - +- posts - +- comments - +- categorizations - - # cascaded in two levels with two has_many associations - >> Author.find(:all, :include=>{:posts=>[:comments, :categorizations]}) - => authors - +- posts - +- comments - +- categorizations - - # cascaded in three levels - >> Company.find(:all, :include=>{:groups=>{:members=>{:favorites}}}) - => companies - +- groups - +- members - +- favorites - -* Make counter cache work when replacing an association #3245 *eugenol@gmail.com* - -* Make migrations verbose *Jamis Buck* - -* Make counter_cache work with polymorphic belongs_to *Jamis Buck* - -* Fixed that calling HasOneProxy#build_model repeatedly would cause saving to happen #4058 *anna@wota.jp* - -* Added Sybase database adapter that relies on the Sybase Open Client bindings (see http://raa.ruby-lang.org/project/sybase-ctlib) #3765 [John Sheets]. It's almost completely Active Record compliant (including migrations), but has the following caveats: - - * Does not support DATE SQL column types; use DATETIME instead. - * Date columns on HABTM join tables are returned as String, not Time. - * Insertions are potentially broken for :polymorphic join tables - * BLOB column access not yet fully supported - -* Clear stale, cached connections left behind by defunct threads. *Jeremy Kemper* - -* CHANGED DEFAULT: set ActiveRecord::Base.allow_concurrency to false. Most AR usage is in single-threaded applications. *Jeremy Kemper* - -* Renamed the "oci" adapter to "oracle", but kept the old name as an alias #4017 *Michael Schoen* - -* Fixed that Base.save should always return false if the save didn't succeed, including if it has halted by before_save's #1861, #2477 *David Heinemeier Hansson* - -* Speed up class -> connection caching and stale connection verification. #3979 *Stefan Kaes* - -* Add set_fixture_class to allow the use of table name accessors with models which use set_table_name. *Kevin Clark* - -* Added that fixtures to placed in subdirectories of the main fixture files are also loaded #3937 *dblack@wobblini.net* - -* Define attribute query methods to avoid method_missing calls. #3677 *Jonathan Viney* - -* ActiveRecord::Base.remove_connection explicitly closes database connections and doesn't corrupt the connection cache. Introducing the disconnect! instance method for the PostgreSQL, MySQL, and SQL Server adapters; implementations for the others are welcome. #3591 *Simon Stapleton, Tom Ward* - -* Added support for nested scopes #3407 [anna@wota.jp]. Examples: - - Developer.with_scope(:find => { :conditions => "salary > 10000", :limit => 10 }) do - Developer.find(:all) # => SELECT * FROM developers WHERE (salary > 10000) LIMIT 10 - - # inner rule is used. (all previous parameters are ignored) - Developer.with_exclusive_scope(:find => { :conditions => "name = 'Jamis'" }) do - Developer.find(:all) # => SELECT * FROM developers WHERE (name = 'Jamis') - end - - # parameters are merged - Developer.with_scope(:find => { :conditions => "name = 'Jamis'" }) do - Developer.find(:all) # => SELECT * FROM developers WHERE (( salary > 10000 ) AND ( name = 'Jamis' )) LIMIT 10 - end - end - -* Fixed db2 connection with empty user_name and auth options #3622 *phurley@gmail.com* - -* Fixed validates_length_of to work on UTF-8 strings by using characters instead of bytes #3699 *Masao Mutoh* - -* Fixed that reflections would bleed across class boundaries in single-table inheritance setups #3796 *Lars Pind* - -* Added calculations: Base.count, Base.average, Base.sum, Base.minimum, Base.maxmium, and the generic Base.calculate. All can be used with :group and :having. Calculations and statitics need no longer require custom SQL. #3958 [Rick Olson]. Examples: - - Person.average :age - Person.minimum :age - Person.maximum :age - Person.sum :salary, :group => :last_name - -* Renamed Errors#count to Errors#size but kept an alias for the old name (and included an alias for length too) #3920 *Luke Redpath* - -* Reflections don't attempt to resolve module nesting of association classes. Simplify type computation. *Jeremy Kemper* - -* Improved the Oracle OCI Adapter with better performance for column reflection (from #3210), fixes to migrations (from #3476 and #3742), tweaks to unit tests (from #3610), and improved documentation (from #2446) #3879 *Aggregated by schoenm@earthlink.net* - -* Fixed that the schema_info table used by ActiveRecord::Schema.define should respect table pre- and suffixes #3834 *rubyonrails@atyp.de* - -* Added :select option to Base.count that'll allow you to select something else than * to be counted on. Especially important for count queries using DISTINCT #3839 *Stefan Kaes* - -* Correct syntax error in mysql DDL, and make AAACreateTablesTest run first *Bob Silva* - -* Allow :include to be used with has_many :through associations #3611 *Michael Schoen* - -* PostgreSQL: smarter schema dumps using pk_and_sequence_for(table). #2920 *Blair Zajac* - -* SQLServer: more compatible limit/offset emulation. #3779 *Tom Ward* - -* Polymorphic join support for has_one associations (has_one :foo, :as => :bar) #3785 *Rick Olson* - -* PostgreSQL: correctly parse negative integer column defaults. #3776 *bellis@deepthought.org* - -* Fix problems with count when used with :include *Jeremy Hopple and Kevin Clark* - -* ActiveRecord::RecordInvalid now states which validations failed in its default error message *Tobias Lütke* - -* Using AssociationCollection#build with arrays of hashes should call build, not create *David Heinemeier Hansson* - -* Remove definition of reloadable? from ActiveRecord::Base to make way for new Reloadable code. *Nicholas Seckar* - -* Fixed schema handling for DB2 adapter that didn't work: an initial schema could be set, but it wasn't used when getting tables and indexes #3678 *Maik Schmidt* - -* Support the :column option for remove_index with the PostgreSQL adapter. #3661 *Shugo Maeda* - -* Add documentation for add_index and remove_index. #3600 *Manfred Stienstra <m.stienstra@fngtps.com>* - -* If the OCI library is not available, raise an exception indicating as much. #3593 *Michael Schoen* - -* Add explicit :order in finder tests as postgresql orders results differently by default. #3577. *Rick Olson* - -* Make dynamic finders honor additional passed in :conditions. #3569 *Oleg Pudeyev <pudeyo@rpi.edu>, Marcel Molina Jr.* - -* Show a meaningful error when the DB2 adapter cannot be loaded due to missing dependencies. *Nicholas Seckar* - -* Make .count work for has_many associations with multi line finder sql *Michael Schoen* - -* Add AR::Base.base_class for querying the ancestor AR::Base subclass *Jamis Buck* - -* Allow configuration of the column used for optimistic locking *wilsonb@gmail.com* - -* Don't hardcode 'id' in acts as list. *ror@philippeapril.com* - -* Fix date errors for SQLServer in association tests. #3406 *Kevin Clark* - -* Escape database name in MySQL adapter when creating and dropping databases. #3409 *anna@wota.jp* - -* Disambiguate table names for columns in validates_uniqueness_of's WHERE clause. #3423 *alex.borovsky@gmail.com* - -* .with_scope imposed create parameters now bypass attr_protected *Tobias Lütke* - -* Don't raise an exception when there are more keys than there are named bind variables when sanitizing conditions. *Marcel Molina Jr.* - -* Multiple enhancements and adjustments to DB2 adaptor. #3377 *contact@maik-schmidt.de* - -* Sanitize scoped conditions. *Marcel Molina Jr.* - -* Added option to Base.reflection_of_all_associations to specify a specific association to scope the call. For example Base.reflection_of_all_associations(:has_many) *David Heinemeier Hansson* - -* Added ActiveRecord::SchemaDumper.ignore_tables which tells SchemaDumper which tables to ignore. Useful for tables with funky column like the ones required for tsearch2. *Tobias Lütke* - -* SchemaDumper now doesn't fail anymore when there are unknown column types in the schema. Instead the table is ignored and a Comment is left in the schema.rb. *Tobias Lütke* - -* Fixed that saving a model with multiple habtm associations would only save the first one. #3244 *yanowitz-rubyonrails@quantumfoam.org, Florian Weber* - -* Fix change_column to work with PostgreSQL 7.x and 8.x. #3141 *wejn@box.cz, Rick Olson, Scott Barron* - -* removed :piggyback in favor of just allowing :select on :through associations. *Tobias Lütke* - -* made method missing delegation to class methods on relation target work on :through associations. *Tobias Lütke* - -* made .find() work on :through relations. *Tobias Lütke* - -* Fix typo in association docs. #3296. *Blair Zajac* - -* Fixed :through relations when using STI inherited classes would use the inherited class's name as foreign key on the join model *Tobias Lütke* - -## 1.13.2 (December 13th, 2005) ## - -* Become part of Rails 1.0 - -* MySQL: allow encoding option for mysql.rb driver. *Jeremy Kemper* - -* Added option inheritance for find calls on has_and_belongs_to_many and has_many assosociations [David Heinemeier Hansson]. Example: - - class Post - has_many :recent_comments, :class_name => "Comment", :limit => 10, :include => :author - end - - post.recent_comments.find(:all) # Uses LIMIT 10 and includes authors - post.recent_comments.find(:all, :limit => nil) # Uses no limit but include authors - post.recent_comments.find(:all, :limit => nil, :include => nil) # Uses no limit and doesn't include authors - -* Added option to specify :group, :limit, :offset, and :select options from find on has_and_belongs_to_many and has_many assosociations *David Heinemeier Hansson* - -* MySQL: fixes for the bundled mysql.rb driver. #3160 *Justin Forder* - -* SQLServer: fix obscure optimistic locking bug. #3068 *kajism@yahoo.com* - -* SQLServer: support uniqueidentifier columns. #2930 *keithm@infused.org* - -* SQLServer: cope with tables names qualified by owner. #3067 *jeff@ministrycentered.com* - -* SQLServer: cope with columns with "desc" in the name. #1950 *Ron Lusk, Ryan Tomayko* - -* SQLServer: cope with primary keys with "select" in the name. #3057 *rdifrango@captechventures.com* - -* Oracle: active? performs a select instead of a commit. #3133 *Michael Schoen* - -* MySQL: more robust test for nullified result hashes. #3124 *Stefan Kaes* - -* Reloading an instance refreshes its aggregations as well as its associations. #3024 *François Beausoleil* - -* Fixed that using :include together with :conditions array in Base.find would cause NoMethodError #2887 *Paul Hammmond* - -* PostgreSQL: more robust sequence name discovery. #3087 *Rick Olson* - -* Oracle: use syntax compatible with Oracle 8. #3131 *Michael Schoen* - -* MySQL: work around ruby-mysql/mysql-ruby inconsistency with mysql.stat. Eliminate usage of mysql.ping because it doesn't guarantee reconnect. Explicitly close and reopen the connection instead. *Jeremy Kemper* - -* Added preliminary support for polymorphic associations *David Heinemeier Hansson* -* Added preliminary support for join models *David Heinemeier Hansson* - -* Allow validate_uniqueness_of to be scoped by more than just one column. #1559. *jeremy@jthopple.com, Marcel Molina Jr.* - -* Firebird: active? and reconnect! methods for handling stale connections. #428 *Ken Kunz <kennethkunz@gmail.com>* - -* Firebird: updated for FireRuby 0.4.0. #3009 *Ken Kunz <kennethkunz@gmail.com>* - -* MySQL and PostgreSQL: active? compatibility with the pure-Ruby driver. #428 *Jeremy Kemper* - -* Oracle: active? check pings the database rather than testing the last command status. #428 *Michael Schoen* - -* SQLServer: resolve column aliasing/quoting collision when using limit or offset in an eager find. #2974 *kajism@yahoo.com* - -* Reloading a model doesn't lose track of its connection. #2996 *junk@miriamtech.com, Jeremy Kemper* - -* Fixed bug where using update_attribute after pushing a record to a habtm association of the object caused duplicate rows in the join table. #2888 *colman@rominato.com, Florian Weber, Michael Schoen* - -* MySQL, PostgreSQL: reconnect! also reconfigures the connection. Otherwise, the connection 'loses' its settings if it times out and is reconnected. #2978 *Shugo Maeda* - -* has_and_belongs_to_many: use JOIN instead of LEFT JOIN. *Jeremy Kemper* - -* MySQL: introduce :encoding option to specify the character set for client, connection, and results. Only available for MySQL 4.1 and later with the mysql-ruby driver. Do SHOW CHARACTER SET in mysql client to see available encodings. #2975 *Shugo Maeda* - -* Add tasks to create, drop and rebuild the MySQL and PostgreSQL test databases. *Marcel Molina Jr.* - -* Correct boolean handling in generated reader methods. #2945 *Don Park, Stefan Kaes* - -* Don't generate read methods for columns whose names are not valid ruby method names. #2946 *Stefan Kaes* - -* Document :force option to create_table. #2921 *Blair Zajac <blair@orcaware.com>* - -* Don't add the same conditions twice in has_one finder sql. #2916 *Jeremy Evans* - -* Rename Version constant to VERSION. #2802 *Marcel Molina Jr.* - -* Introducing the Firebird adapter. Quote columns and use attribute_condition more consistently. Setup guide: http://wiki.rubyonrails.com/rails/pages/Firebird+Adapter #1874 *Ken Kunz <kennethkunz@gmail.com>* - -* SQLServer: active? and reconnect! methods for handling stale connections. #428 *kajism@yahoo.com, Tom Ward <tom@popdog.net>* - -* Associations handle case-equality more consistently: item.parts.is_a?(Array) and item.parts === Array. #1345 *MarkusQ@reality.com* - -* SQLServer: insert uses given primary key value if not nil rather than SELECT @@IDENTITY. #2866 *kajism@yahoo.com, Tom Ward <tom@popdog.net>* - -* Oracle: active? and reconnect! methods for handling stale connections. Optionally retry queries after reconnect. #428 *Michael Schoen <schoenm@earthlink.net>* - -* Correct documentation for Base.delete_all. #1568 *Newhydra* - -* Oracle: test case for column default parsing. #2788 *Michael Schoen <schoenm@earthlink.net>* - -* Update documentation for Migrations. #2861 *Tom Werner <tom@cube6media.com>* - -* When AbstractAdapter#log rescues an exception, attempt to detect and reconnect to an inactive database connection. Connection adapter must respond to the active? and reconnect! instance methods. Initial support for PostgreSQL, MySQL, and SQLite. Make certain that all statements which may need reconnection are performed within a logged block: for example, this means no avoiding log(sql, name) { } if @logger.nil? #428 *Jeremy Kemper* - -* Oracle: Much faster column reflection. #2848 *Michael Schoen <schoenm@earthlink.net>* - -* Base.reset_sequence_name analogous to reset_table_name (mostly useful for testing). Base.define_attr_method allows nil values. *Jeremy Kemper* - -* PostgreSQL: smarter sequence name defaults, stricter last_insert_id, warn on pk without sequence. *Jeremy Kemper* - -* PostgreSQL: correctly discover custom primary key sequences. #2594 *Blair Zajac <blair@orcaware.com>, meadow.nnick@gmail.com, Jeremy Kemper* - -* SQLServer: don't report limits for unsupported field types. #2835 *Ryan Tomayko* - -* Include the Enumerable module in ActiveRecord::Errors. *Rick Bradley <rick@rickbradley.com>* - -* Add :group option, correspond to GROUP BY, to the find method and to the has_many association. #2818 *rubyonrails@atyp.de* - -* Don't cast nil or empty strings to a dummy date. #2789 *Rick Bradley <rick@rickbradley.com>* - -* acts_as_list plays nicely with inheritance by remembering the class which declared it. #2811 *rephorm@rephorm.com* - -* Fix sqlite adaptor's detection of missing dbfile or database declaration. *Nicholas Seckar* - -* Fixed acts_as_list for definitions without an explicit :order #2803 *Jonathan Viney* - -* Upgrade bundled ruby-mysql 0.2.4 with mysql411 shim (see #440) to ruby-mysql 0.2.6 with a patchset for 4.1 protocol support. Local change [301] is now a part of the main driver; reapplied local change [2182]. Removed GC.start from Result.free. *tommy@tmtm.org, akuroda@gmail.com, Doug Fales <doug.fales@gmail.com>, Jeremy Kemper* - -* Correct handling of complex order clauses with SQL Server limit emulation. #2770 *Tom Ward <tom@popdog.net>, Matt B.* - -* Correct whitespace problem in Oracle default column value parsing. #2788 *rick@rickbradley.com* - -* Destroy associated has_and_belongs_to_many records after all before_destroy callbacks but before destroy. This allows you to act on the habtm association as you please while preserving referential integrity. #2065 *larrywilliams1@gmail.com, sam.kirchmeier@gmail.com, elliot@townx.org, Jeremy Kemper* - -* Deprecate the old, confusing :exclusively_dependent option in favor of :dependent => :delete_all. *Jeremy Kemper* - -* More compatible Oracle column reflection. #2771 *Ryan Davis <ryand-ruby@zenspider.com>, Michael Schoen <schoenm@earthlink.net>* - - -## 1.13.0 (November 7th, 2005) ## - -* Fixed faulty regex in get_table_name method (SQLServerAdapter) #2639 *Ryan Tomayko* - -* Added :include as an option for association declarations [David Heinemeier Hansson]. Example: - - has_many :posts, :include => [ :author, :comments ] - -* Rename Base.constrain to Base.with_scope so it doesn't conflict with existing concept of database constraints. Make scoping more robust: uniform method => parameters, validated method names and supported finder parameters, raise exception on nested scopes. [Jeremy Kemper] Example: - - Comment.with_scope(:find => { :conditions => 'active=true' }, :create => { :post_id => 5 }) do - # Find where name = ? and active=true - Comment.find :all, :conditions => ['name = ?', name] - # Create comment associated with :post_id - Comment.create :body => "Hello world" - end - -* Fixed that SQL Server should ignore :size declarations on anything but integer and string in the agnostic schema representation #2756 *Ryan Tomayko* - -* Added constrain scoping for creates using a hash of attributes bound to the :creation key [David Heinemeier Hansson]. Example: - - Comment.constrain(:creation => { :post_id => 5 }) do - # Associated with :post_id - Comment.create :body => "Hello world" - end - - This is rarely used directly, but allows for find_or_create on associations. So you can do: - - # If the tag doesn't exist, a new one is created that's associated with the person - person.tags.find_or_create_by_name("Summer") - -* Added find_or_create_by_X as a second type of dynamic finder that'll create the record if it doesn't already exist [David Heinemeier Hansson]. Example: - - # No 'Summer' tag exists - Tag.find_or_create_by_name("Summer") # equal to Tag.create(:name => "Summer") - - # Now the 'Summer' tag does exist - Tag.find_or_create_by_name("Summer") # equal to Tag.find_by_name("Summer") - -* Added extension capabilities to has_many and has_and_belongs_to_many proxies [David Heinemeier Hansson]. Example: - - class Account < ActiveRecord::Base - has_many :people do - def find_or_create_by_name(name) - first_name, *last_name = name.split - last_name = last_name.join " " - - find_or_create_by_first_name_and_last_name(first_name, last_name) - end - end - end - - person = Account.find(:first).people.find_or_create_by_name("David Heinemeier Hansson") - person.first_name # => "David" - person.last_name # => "Heinemeier Hansson" - - Note that the anoymous module must be declared using brackets, not do/end (due to order of evaluation). - -* Omit internal dtproperties table from SQLServer table list. #2729 *Ryan Tomayko* - -* Quote column names in generated SQL. #2728 *Ryan Tomayko* - -* Correct the pure-Ruby MySQL 4.1.1 shim's version test. #2718 *Jeremy Kemper* - -* Add Model.create! to match existing model.save! method. When save! raises RecordInvalid, you can catch the exception, retrieve the invalid record (invalid_exception.record), and see its errors (invalid_exception.record.errors). *Jeremy Kemper* - -* Correct fixture behavior when table name pluralization is off. #2719 *Rick Bradley <rick@rickbradley.com>* - -* Changed :dbfile to :database for SQLite adapter for consistency (old key still works as an alias) #2644 *Dan Peterson* - -* Added migration support for Oracle #2647 *Michael Schoen* - -* Worked around that connection can't be reset if allow_concurrency is off. #2648 *Michael Schoen <schoenm@earthlink.net>* - -* Fixed SQL Server adapter to pass even more tests and do even better #2634 *Ryan Tomayko* - -* Fixed SQL Server adapter so it honors options[:conditions] when applying :limits #1978 *Tom Ward* - -* Added migration support to SQL Server adapter (please someone do the same for Oracle and DB2) #2625 *Tom Ward* - -* Use AR::Base.silence rather than AR::Base.logger.silence in fixtures to preserve Log4r compatibility. #2618 *dansketcher@gmail.com* - -* Constraints are cloned so they can't be inadvertently modified while they're - in effect. Added :readonly finder constraint. Calling an association collection's class method (Part.foobar via item.parts.foobar) constrains :readonly => false since the collection's :joins constraint would otherwise force it to true. [Jeremy Kemper <rails@bitsweat.net>] -* Added :offset and :limit to the kinds of options that Base.constrain can use #2466 *duane.johnson@gmail.com* - -* Fixed handling of nil number columns on Oracle and cleaned up tests for Oracle in general #2555 *Michael Schoen* - -* Added quoted_true and quoted_false methods and tables to db2_adapter and cleaned up tests for DB2 #2493, #2624 *maik schmidt* - - -## 1.12.2 (October 26th, 2005) ## - -* Allow symbols to rename columns when using SQLite adapter. #2531 *Kevin Clark* - -* Map Active Record time to SQL TIME. #2575, #2576 *Robby Russell <robby@planetargon.com>* - -* Clarify semantics of ActiveRecord::Base#respond_to? #2560 *Stefan Kaes* - -* Fixed Association#clear for associations which have not yet been accessed. #2524 *Patrick Lenz <patrick@lenz.sh>* - -* HABTM finders shouldn't return readonly records. #2525 *Patrick Lenz <patrick@lenz.sh>* - -* Make all tests runnable on their own. #2521. *Blair Zajac <blair@orcaware.com>* - - -## 1.12.1 (October 19th, 2005) ## - -* Always parenthesize :conditions options so they may be safely combined with STI and constraints. - -* Correct PostgreSQL primary key sequence detection. #2507 *tmornini@infomania.com* - -* Added support for using limits in eager loads that involve has_many and has_and_belongs_to_many associations - - -## 1.12.0 (October 16th, 2005) ## - -* Update/clean up documentation (rdoc) - -* PostgreSQL sequence support. Use set_sequence_name in your model class to specify its primary key sequence. #2292 *Rick Olson <technoweenie@gmail.com>, Robby Russell <robby@planetargon.com>* - -* Change default logging colors to work on both white and black backgrounds. *Sam Stephenson* - -* YAML fixtures support ordered hashes for fixtures with foreign key dependencies in the same table. #1896 *purestorm@ggnore.net* - -* :dependent now accepts :nullify option. Sets the foreign key of the related objects to NULL instead of deleting them. #2015 *Robby Russell <robby@planetargon.com>* - -* Introduce read-only records. If you call object.readonly! then it will mark the object as read-only and raise ReadOnlyRecord if you call object.save. object.readonly? reports whether the object is read-only. Passing :readonly => true to any finder method will mark returned records as read-only. The :joins option now implies :readonly, so if you use this option, saving the same record will now fail. Use find_by_sql to work around. - -* Avoid memleak in dev mode when using fcgi - -* Simplified .clear on active record associations by using the existing delete_records method. #1906 *Caleb <me@cpb.ca>* - -* Delegate access to a customized primary key to the conventional id method. #2444. *Blair Zajac <blair@orcaware.com>* - -* Fix errors caused by assigning a has-one or belongs-to property to itself - -* Add ActiveRecord::Base.schema_format setting which specifies how databases should be dumped *Sam Stephenson* - -* Update DB2 adapter. #2206. *contact@maik-schmidt.de* - -* Corrections to SQLServer native data types. #2267. *rails.20.clarry@spamgourmet.com* - -* Deprecated ActiveRecord::Base.threaded_connection in favor of ActiveRecord::Base.allow_concurrency. - -* Protect id attribute from mass assigment even when the primary key is set to something else. #2438. *Blair Zajac <blair@orcaware.com>* - -* Misc doc fixes (typos/grammar/etc.). #2430. *coffee2code* - -* Add test coverage for content_columns. #2432. *coffee2code* - -* Speed up for unthreaded environments. #2431. *Stefan Kaes* - -* Optimization for Mysql selects using mysql-ruby extension greater than 2.6.3. #2426. *Stefan Kaes* - -* Speed up the setting of table_name. #2428. *Stefan Kaes* - -* Optimize instantiation of STI subclass records. In partial fullfilment of #1236. *Stefan Kaes* - -* Fix typo of 'constrains' to 'contraints'. #2069. *Michael Schuerig <michael@schuerig.de>* - -* Optimization refactoring for add_limit_offset!. In partial fullfilment of #1236. *Stefan Kaes* - -* Add ability to get all siblings, including the current child, with acts_as_tree. Recloses #2140. *Michael Schuerig <michael@schuerig.de>* - -* Add geometric type for postgresql adapter. #2233 *Andrew Kaspick* - -* Add option (true by default) to generate reader methods for each attribute of a record to avoid the overhead of calling method missing. In partial fullfilment of #1236. *Stefan Kaes* - -* Add convenience predicate methods on Column class. In partial fullfilment of #1236. *Stefan Kaes* - -* Raise errors when invalid hash keys are passed to ActiveRecord::Base.find. #2363 *Chad Fowler <chad@chadfowler.com>, Nicholas Seckar* - -* Added :force option to create_table that'll try to drop the table if it already exists before creating - -* Fix transactions so that calling return while inside a transaction will not leave an open transaction on the connection. *Nicholas Seckar* - -* Use foreign_key inflection uniformly. #2156 *Blair Zajac <blair@orcaware.com>* - -* model.association.clear should destroy associated objects if :dependent => true instead of nullifying their foreign keys. #2221 *joergd@pobox.com, ObieFernandez <obiefernandez@gmail.com>* - -* Returning false from before_destroy should cancel the action. #1829 *Jeremy Huffman* - -* Recognize PostgreSQL NOW() default as equivalent to CURRENT_TIMESTAMP or CURRENT_DATE, depending on the column's type. #2256 *mat <mat@absolight.fr>* - -* Extensive documentation for the abstract database adapter. #2250 *François Beausoleil <fbeausoleil@ftml.net>* - -* Clean up Fixtures.reset_sequences for PostgreSQL. Handle tables with no rows and models with custom primary keys. #2174, #2183 *jay@jay.fm, Blair Zajac <blair@orcaware.com>* - -* Improve error message when nil is assigned to an attr which validates_size_of within a range. #2022 *Manuel Holtgrewe <purestorm@ggnore.net>* - -* Make update_attribute use the same writer method that update_attributes uses. - \#2237 [trevor@protocool.com] -* Make migrations honor table name prefixes and suffixes. #2298 *Jakob Skjerning, Marcel Molina Jr.* - -* Correct and optimize PostgreSQL bytea escaping. #1745, #1837 *dave@cherryville.org, ken@miriamtech.com, bellis@deepthought.org* - -* Fixtures should only reset a PostgreSQL sequence if it corresponds to an integer primary key named id. #1749 *chris@chrisbrinker.com* - -* Standardize the interpretation of boolean columns in the Mysql and Sqlite adapters. (Use MysqlAdapter.emulate_booleans = false to disable this behavior) - -* Added new symbol-driven approach to activating observers with Base#observers= [David Heinemeier Hansson]. Example: - - ActiveRecord::Base.observers = :cacher, :garbage_collector - -* Added AbstractAdapter#select_value and AbstractAdapter#select_values as convenience methods for selecting single values, instead of hashes, of the first column in a SELECT #2283 *solo@gatelys.com* - -* Wrap :conditions in parentheses to prevent problems with OR's #1871 *Jamis Buck* - -* Allow the postgresql adapter to work with the SchemaDumper. *Jamis Buck* - -* Add ActiveRecord::SchemaDumper for dumping a DB schema to a pure-ruby file, making it easier to consolidate large migration lists and port database schemas between databases. *Jamis Buck* - -* Fixed migrations for Windows when using more than 10 *David Naseby* - -* Fixed that the create_x method from belongs_to wouldn't save the association properly #2042 *Florian Weber* - -* Fixed saving a record with two unsaved belongs_to associations pointing to the same object #2023 *Tobias Lütke* - -* Improved migrations' behavior when the schema_info table is empty. *Nicholas Seckar* - -* Fixed that Observers didn't observe sub-classes #627 *Florian Weber* - -* Fix eager loading error messages, allow :include to specify tables using strings or symbols. Closes #2222 *Marcel Molina Jr.* - -* Added check for RAILS_CONNECTION_ADAPTERS on startup and only load the connection adapters specified within if its present (available in Rails through config.connection_adapters using the new config) #1958 *skae* - -* Fixed various problems with has_and_belongs_to_many when using customer finder_sql #2094 *Florian Weber* - -* Added better exception error when unknown column types are used with migrations #1814 *François Beausoleil* - -* Fixed "connection lost" issue with the bundled Ruby/MySQL driver (would kill the app after 8 hours of inactivity) #2163, #428 *kajism@yahoo.com* - -* Fixed comparison of Active Record objects so two new objects are not equal #2099 *deberg* - -* Fixed that the SQL Server adapter would sometimes return DBI::Timestamp objects instead of Time #2127 *Tom Ward* - -* Added the instance methods #root and #ancestors on acts_as_tree and fixed siblings to not include the current node #2142, #2140 *coffee2code* - -* Fixed that Active Record would call SHOW FIELDS twice (or more) for the same model when the cached results were available #1947 *sd@notso.net* - -* Added log_level and use_silence parameter to ActiveRecord::Base.benchmark. The first controls at what level the benchmark statement will be logged (now as debug, instead of info) and the second that can be passed false to include all logging statements during the benchmark block/ - -* Make sure the schema_info table is created before querying the current version #1903 - -* Fixtures ignore table name prefix and suffix #1987 *Jakob Skjerning* - -* Add documentation for index_type argument to add_index method for migrations #2005 *Blaine* - -* Modify read_attribute to allow a symbol argument #2024 *Ken Kunz* - -* Make destroy return self #1913 *Sebastian Kanthak* - -* Fix typo in validations documentation #1938 *court3nay* - -* Make acts_as_list work for insert_at(1) #1966 *hensleyl@papermountain.org* - -* Fix typo in count_by_sql documentation #1969 *Alexey Verkhovsky* - -* Allow add_column and create_table to specify NOT NULL #1712 *emptysands@gmail.com* - -* Fix create_table so that id column is implicitly added *Rick Olson* - -* Default sequence names for Oracle changed to #{table_name}_seq, which is the most commonly used standard. In addition, a new method ActiveRecord::Base#set_sequence_name allows the developer to set the sequence name per model. This is a non-backwards-compatible change -- anyone using the old-style "rails_sequence" will need to either create new sequences, or set: ActiveRecord::Base.set_sequence_name = "rails_sequence" #1798 - -* OCIAdapter now properly handles synonyms, which are commonly used to separate out the schema owner from the application user #1798 - -* Fixed the handling of camelCase columns names in Oracle #1798 - -* Implemented for OCI the Rakefile tasks of :clone_structure_to_test, :db_structure_dump, and :purge_test_database, which enable Oracle folks to enjoy all the agile goodness of Rails for testing. Note that the current implementation is fairly limited -- only tables and sequences are cloned, not constraints or indexes. A full clone in Oracle generally requires some manual effort, and is version-specific. Post 9i, Oracle recommends the use of the DBMS_METADATA package, though that approach requires editing of the physical characteristics generated #1798 - -* Fixed the handling of multiple blob columns in Oracle if one or more of them are null #1798 - -* Added support for calling constrained class methods on has_many and has_and_belongs_to_many collections #1764 *Tobias Lütke* - - class Comment < AR:B - def self.search(q) - find(:all, :conditions => ["body = ?", q]) - end - end - - class Post < AR:B - has_many :comments - end - - Post.find(1).comments.search('hi') # => SELECT * from comments WHERE post_id = 1 AND body = 'hi' - - NOTICE: This patch changes the underlying SQL generated by has_and_belongs_to_many queries. If your relying on that, such as - by explicitly referencing the old t and j aliases, you'll need to update your code. Of course, you _shouldn't_ be relying on - details like that no less than you should be diving in to touch private variables. But just in case you do, consider yourself - noticed :) - -* Added migration support for SQLite (using temporary tables to simulate ALTER TABLE) #1771 *Sam Stephenson* - -* Remove extra definition of supports_migrations? from abstract_adaptor.rb *Nicholas Seckar* - -* Fix acts_as_list so that moving next-to-last item to the bottom does not result in duplicate item positions - -* Fixed incompatibility in DB2 adapter with the new limit/offset approach #1718 *Maik Schmidt* - -* Added :select option to find which can specify a different value than the default *, like find(:all, :select => "first_name, last_name"), if you either only want to select part of the columns or exclude columns otherwise included from a join #1338 *Stefan Kaes* - - -## 1.11.1 (11 July, 2005) ## - -* Added support for limit and offset with eager loading of has_one and belongs_to associations. Using the options with has_many and has_and_belongs_to_many associations will now raise an ActiveRecord::ConfigurationError #1692 *Rick Olson* - -* Fixed that assume_bottom_position (in acts_as_list) could be called on items already last in the list and they would move one position away from the list #1648 *tyler@kianta.com* - -* Added ActiveRecord::Base.threaded_connections flag to turn off 1-connection per thread (required for thread safety). By default it's on, but WEBrick in Rails need it off #1685 *Sam Stephenson* - -* Correct reflected table name for singular associations. #1688 *court3nay* - -* Fixed optimistic locking with SQL Server #1660 *tom@popdog.net* - -* Added ActiveRecord::Migrator.migrate that can figure out whether to go up or down based on the target version and the current - -* Added better error message for "packets out of order" #1630 *court3nay* - -* Fixed first run of "rake migrate" on PostgreSQL by not expecting a return value on the id #1640 - - -## 1.11.0 (6 July, 2005) ## - -* Fixed that Yaml error message in fixtures hid the real error #1623 *Nicholas Seckar* - -* Changed logging of SQL statements to use the DEBUG level instead of INFO - -* Added new Migrations framework for describing schema transformations in a way that can be easily applied across multiple databases #1604 [Tobias Lütke] See documentation under ActiveRecord::Migration and the additional support in the Rails rakefile/generator. - -* Added callback hooks to association collections #1549 [Florian Weber]. Example: - - class Project - has_and_belongs_to_many :developers, :before_add => :evaluate_velocity - - def evaluate_velocity(developer) - ... - end - end - - ..raising an exception will cause the object not to be added (or removed, with before_remove). - - -* Fixed Base.content_columns call for SQL Server adapter #1450 *DeLynn Berry* - -* Fixed Base#write_attribute to work with both symbols and strings #1190 *Paul Legato* - -* Fixed that has_and_belongs_to_many didn't respect single table inheritance types #1081 *Florian Weber* - -* Speed up ActiveRecord#method_missing for the common case (read_attribute). - -* Only notify observers on after_find and after_initialize if these methods are defined on the model. #1235 *Stefan Kaes* - -* Fixed that single-table inheritance sub-classes couldn't be used to limit the result set with eager loading #1215 *Chris McGrath* - -* Fixed validates_numericality_of to work with overrided getter-method when :allow_nil is on #1316 *raidel@onemail.at* - -* Added roots, root, and siblings to the batch of methods added by acts_as_tree #1541 *Michael Schuerig* - -* Added support for limit/offset with the MS SQL Server driver so that pagination will now work #1569 *DeLynn Berry* - -* Added support for ODBC connections to MS SQL Server so you can connect from a non-Windows machine #1569 *Mark Imbriaco/DeLynn Berry* - -* Fixed that multiparameter posts ignored attr_protected #1532 *alec+rails@veryclever.net* - -* Fixed problem with eager loading when using a has_and_belongs_to_many association using :association_foreign_key #1504 *flash@vanklinkenbergsoftware.nl* - -* Fixed Base#find to honor the documentation on how :joins work and make them consistent with Base#count #1405 [pritchie@gmail.com]. What used to be: - - Developer.find :all, :joins => 'developers_projects', :conditions => 'id=developer_id AND project_id=1' - - ...should instead be: - - Developer.find( - :all, - :joins => 'LEFT JOIN developers_projects ON developers.id = developers_projects.developer_id', - :conditions => 'project_id=1' - ) - -* Fixed that validations didn't respecting custom setting for too_short, too_long messages #1437 *Marcel Molina Jr.* - -* Fixed that clear_association_cache doesn't delete new associations on new records (so you can safely place new records in the session with Action Pack without having new associations wiped) #1494 *cluon* - -* Fixed that calling Model.find([]) returns [] and doesn't throw an exception #1379 - -* Fixed that adding a record to a has_and_belongs_to collection would always save it -- now it only saves if its a new record #1203 *Alisdair McDiarmid* - -* Fixed saving of in-memory association structures to happen as an after_create/after_update callback instead of after_save -- that way you can add new associations in after_create/after_update callbacks without getting them saved twice - -* Allow any Enumerable, not just Array, to work as bind variables #1344 *Jeremy Kemper* - -* Added actual database-changing behavior to collection assigment for has_many and has_and_belongs_to_many #1425 [Sebastian Kanthak]. - Example: - - david.projects = [Project.find(1), Project.new("name" => "ActionWebSearch")] - david.save - - If david.projects already contain the project with ID 1, this is left unchanged. Any other projects are dropped. And the new - project is saved when david.save is called. - - Also included is a way to do assignments through IDs, which is perfect for checkbox updating, so you get to do: - - david.project_ids = [1, 5, 7] - -* Corrected typo in find SQL for has_and_belongs_to_many. #1312 *ben@bensinclair.com* - -* Fixed sanitized conditions for has_many finder method. #1281 *jackc@hylesanderson.com, pragdave, Tobias Lütke* - -* Comprehensive PostgreSQL schema support. Use the optional schema_search_path directive in database.yml to give a comma-separated list of schemas to search for your tables. This allows you, for example, to have tables in a shared schema without having to use a custom table name. See http://www.postgresql.org/docs/8.0/interactive/ddl-schemas.html to learn more. #827 *dave@cherryville.org* - -* Corrected @@configurations typo #1410 *david@ruppconsulting.com* - -* Return PostgreSQL columns in the order they were declared #1374 *perlguy@gmail.com* - -* Allow before/after update hooks to work on models using optimistic locking - -* Eager loading of dependent has_one associations won't delete the association #1212 - -* Added a second parameter to the build and create method for has_one that controls whether the existing association should be replaced (which means nullifying its foreign key as well). By default this is true, but false can be passed to prevent it. - -* Using transactional fixtures now causes the data to be loaded only once. - -* Added fixture accessor methods that can be used when instantiated fixtures are disabled. - - fixtures :web_sites - - def test_something - assert_equal "Ruby on Rails", web_sites(:rubyonrails).name - end - -* Added DoubleRenderError exception that'll be raised if render* is called twice #518 *Nicholas Seckar* - -* Fixed exceptions occuring after render has been called #1096 *Nicholas Seckar* - -* CHANGED: validates_presence_of now uses Errors#add_on_blank, which will make " " fail the validation where it didn't before #1309 - -* Added Errors#add_on_blank which works like Errors#add_on_empty, but uses Object#blank? instead - -* Added the :if option to all validations that can either use a block or a method pointer to determine whether the validation should be run or not. #1324 [Duane Johnson/jhosteny]. Examples: - - Conditional validations such as the following are made possible: - validates_numericality_of :income, :if => :employed? - - Conditional validations can also solve the salted login generator problem: - validates_confirmation_of :password, :if => :new_password? - - Using blocks: - validates_presence_of :username, :if => Proc.new { |user| user.signup_step > 1 } - -* Fixed use of construct_finder_sql when using :join #1288 *dwlt@dwlt.net* - -* Fixed that :delete_sql in has_and_belongs_to_many associations couldn't access record properties #1299 *Rick Olson* - -* Fixed that clone would break when an aggregate had the same name as one of its attributes #1307 *Jeremy Kemper* - -* Changed that destroying an object will only freeze the attributes hash, which keeps the object from having attributes changed (as that wouldn't make sense), but allows for the querying of associations after it has been destroyed. - -* Changed the callbacks such that observers are notified before the in-object callbacks are triggered. Without this change, it wasn't possible to act on the whole object in something like a before_destroy observer without having the objects own callbacks (like deleting associations) called first. - -* Added option for passing an array to the find_all version of the dynamic finders and have it evaluated as an IN fragment. Example: - - # SELECT * FROM topics WHERE title IN ('First', 'Second') - Topic.find_all_by_title(["First", "Second"]) - -* Added compatibility with camelCase column names for dynamic finders #533 *Dee Zsombor* - -* Fixed extraneous comma in count() function that made it not work with joins #1156 *Jarkko Laine/Dee Zsombor* - -* Fixed incompatibility with Base#find with an array of ids that would fail when using eager loading #1186 *Alisdair McDiarmid* - -* Fixed that validate_length_of lost :on option when :within was specified #1195 *jhosteny@mac.com* - -* Added encoding and min_messages options for PostgreSQL #1205 [Shugo Maeda]. Configuration example: - - development: - adapter: postgresql - database: rails_development - host: localhost - username: postgres - password: - encoding: UTF8 - min_messages: ERROR - -* Fixed acts_as_list where deleting an item that was removed from the list would ruin the positioning of other list items #1197 *Jamis Buck* - -* Added validates_exclusion_of as a negative of validates_inclusion_of - -* Optimized counting of has_many associations by setting the association to empty if the count is 0 so repeated calls doesn't trigger database calls - - -## 1.10.1 (20th April, 2005) ## - -* Fixed frivilous database queries being triggered with eager loading on empty associations and other things - -* Fixed order of loading in eager associations - -* Fixed stray comma when using eager loading and ordering together from has_many associations #1143 - - -## 1.10.0 (19th April, 2005) ## - -* Added eager loading of associations as a way to solve the N+1 problem more gracefully without piggy-back queries. Example: - - for post in Post.find(:all, :limit => 100) - puts "Post: " + post.title - puts "Written by: " + post.author.name - puts "Last comment on: " + post.comments.first.created_on - end - - This used to generate 301 database queries if all 100 posts had both author and comments. It can now be written as: - - for post in Post.find(:all, :limit => 100, :include => [ :author, :comments ]) - - ...and the number of database queries needed is now 1. - -* Added new unified Base.find API and deprecated the use of find_first and find_all. See the documentation for Base.find. Examples: - - Person.find(1, :conditions => "administrator = 1", :order => "created_on DESC") - Person.find(1, 5, 6, :conditions => "administrator = 1", :order => "created_on DESC") - Person.find(:first, :order => "created_on DESC", :offset => 5) - Person.find(:all, :conditions => [ "category IN (?)", categories], :limit => 50) - Person.find(:all, :offset => 10, :limit => 10) - -* Added acts_as_nested_set #1000 [wschenk]. Introduction: - - This acts provides Nested Set functionality. Nested Set is similiar to Tree, but with - the added feature that you can select the children and all of it's descendants with - a single query. A good use case for this is a threaded post system, where you want - to display every reply to a comment without multiple selects. - -* Added Base.save! that attempts to save the record just like Base.save but will raise a RecordInvalid exception instead of returning false if the record is not valid *Dave Thomas* - -* Fixed PostgreSQL usage of fixtures with regards to public schemas and table names with dots #962 *gnuman1@gmail.com* - -* Fixed that fixtures were being deleted in the same order as inserts causing FK errors #890 *andrew.john.peters@gmail.com* - -* Fixed loading of fixtures in to be in the right order (or PostgreSQL would bark) #1047 *stephenh@chase3000.com* - -* Fixed page caching for non-vhost applications living underneath the root #1004 *Ben Schumacher* - -* Fixes a problem with the SQL Adapter which was resulting in IDENTITY_INSERT not being set to ON when it should be #1104 *adelle* - -* Added the option to specify the acceptance string in validates_acceptance_of #1106 *caleb@aei-tech.com* - -* Added insert_at(position) to acts_as_list #1083 *DeLynnB* - -* Removed the default order by id on has_and_belongs_to_many queries as it could kill performance on large sets (you can still specify by hand with :order) - -* Fixed that Base.silence should restore the old logger level when done, not just set it to DEBUG #1084 *yon@milliped.com* - -* Fixed boolean saving on Oracle #1093 *mparrish@pearware.org* - -* Moved build_association and create_association for has_one and belongs_to out of deprecation as they work when the association is nil unlike association.build and association.create, which require the association to be already in place #864 - -* Added rollbacks of transactions if they're active as the dispatcher is killed gracefully (TERM signal) #1054 *Leon Bredt* - -* Added quoting of column names for fixtures #997 *jcfischer@gmail.com* - -* Fixed counter_sql when no records exist in database for PostgreSQL (would give error, not 0) #1039 *Caleb Tennis* - -* Fixed that benchmarking times for rendering included db runtimes #987 *Stefan Kaes* - -* Fixed boolean queries for t/f fields in PostgreSQL #995 *dave@cherryville.org* - -* Added that model.items.delete(child) will delete the child, not just set the foreign key to nil, if the child is dependent on the model #978 *Jeremy Kemper* - -* Fixed auto-stamping of dates (created_on/updated_on) for PostgreSQL #985 *dave@cherryville.org* - -* Fixed Base.silence/benchmark to only log if a logger has been configured #986 *Stefan Kaes* - -* Added a join parameter as the third argument to Base.find_first and as the second to Base.count #426, #988 *Stefan Kaes* - -* Fixed bug in Base#hash method that would treat records with the same string-based id as different *Dave Thomas* - -* Renamed DateHelper#distance_of_time_in_words_to_now to DateHelper#time_ago_in_words (old method name is still available as a deprecated alias) - - -## 1.9.1 (27th March, 2005) ## - -* Fixed that Active Record objects with float attribute could not be cloned #808 - -* Fixed that MissingSourceFile's wasn't properly detected in production mode #925 *Nicholas Seckar* - -* Fixed that :counter_cache option would look for a line_items_count column for a LineItem object instead of lineitems_count - -* Fixed that AR exists?() would explode on postgresql if the passed id did not match the PK type #900 *Scott Barron* - -* Fixed the MS SQL adapter to work with the new limit/offset approach and with binary data (still suffering from 7KB limit, though) #901 *delynnb* - - -## 1.9.0 (22th March, 2005) ## - -* Added adapter independent limit clause as a two-element array with the first being the limit, the second being the offset #795 [Sam Stephenson]. Example: - - Developer.find_all nil, 'id ASC', 5 # return the first five developers - Developer.find_all nil, 'id ASC', [3, 8] # return three developers, starting from #8 and forward - - This doesn't yet work with the DB2 or MS SQL adapters. Patches to make that happen are encouraged. - -* Added alias_method :to_param, :id to Base, such that Active Record objects to be used as URL parameters in Action Pack automatically #812 *Nicholas Seckar/Sam Stephenson* - -* Improved the performance of the OCI8 adapter for Oracle #723 *pilx/gjenkins* - -* Added type conversion before saving a record, so string-based values like "10.0" aren't left for the database to convert #820 *dave@cherryville.org* - -* Added with additional settings for working with transactional fixtures and pre-loaded test databases #865 *mindel* - -* Fixed acts_as_list to trigger remove_from_list on destroy after the fact, not before, so a unique position can be maintained #871 *Alisdair McDiarmid* - -* Added the possibility of specifying fixtures in multiple calls #816 *kim@tinker.com* - -* Added Base.exists?(id) that'll return true if an object of the class with the given id exists #854 *stian@grytoyr.net* - -* Added optionally allow for nil or empty strings with validates_numericality_of #801 *Sebastian Kanthak* - -* Fixed problem with using slashes in validates_format_of regular expressions #801 *Sebastian Kanthak* - -* Fixed that SQLite3 exceptions are caught and reported properly #823 *yerejm* - -* Added that all types of after_find/after_initialized callbacks are triggered if the explicit implementation is present, not only the explicit implementation itself - -* Fixed that symbols can be used on attribute assignment, like page.emails.create(:subject => data.subject, :body => data.body) - - -## 1.8.0 (7th March, 2005) ## - -* Added ActiveRecord::Base.colorize_logging to control whether to use colors in logs or not (on by default) - -* Added support for timestamp with time zone in PostgreSQL #560 *Scott Barron* - -* Added MultiparameterAssignmentErrors and AttributeAssignmentError exceptions #777 [demetrius]. Documentation: - - * +MultiparameterAssignmentErrors+ -- collection of errors that occurred during a mass assignment using the - +attributes=+ method. The +errors+ property of this exception contains an array of +AttributeAssignmentError+ - objects that should be inspected to determine which attributes triggered the errors. - * +AttributeAssignmentError+ -- an error occurred while doing a mass assignment through the +attributes=+ method. - You can inspect the +attribute+ property of the exception object to determine which attribute triggered the error. - -* Fixed that postgresql adapter would fails when reading bytea fields with null value #771 *rodrigo k* - -* Added transactional fixtures that uses rollback to undo changes to fixtures instead of DELETE/INSERT -- it's much faster. See documentation under Fixtures #760 *Jeremy Kemper* - -* Added destruction of dependent objects in has_one associations when a new assignment happens #742 [mindel]. Example: - - class Account < ActiveRecord::Base - has_one :credit_card, :dependent => true - end - class CreditCard < ActiveRecord::Base - belongs_to :account - end - - account.credit_card # => returns existing credit card, lets say id = 12 - account.credit_card = CreditCard.create("number" => "123") - account.save # => CC with id = 12 is destroyed - - -* Added validates_numericality_of #716 [Sebastian Kanthak/Chris McGrath]. Docuemntation: - - Validates whether the value of the specified attribute is numeric by trying to convert it to - a float with Kernel.Float (if <tt>integer</tt> is false) or applying it to the regular expression - <tt>/^[\+\-]?\d+$/</tt> (if <tt>integer</tt> is set to true). - - class Person < ActiveRecord::Base - validates_numericality_of :value, :on => :create - end - - Configuration options: - * <tt>message</tt> - A custom error message (default is: "is not a number") - * <tt>on</tt> Specifies when this validation is active (default is :save, other options :create, :update) - * <tt>only_integer</tt> Specifies whether the value has to be an integer, e.g. an integral value (default is false) - - -* Fixed that HasManyAssociation#count was using :finder_sql rather than :counter_sql if it was available #445 *Scott Barron* - -* Added better defaults for composed_of, so statements like composed_of :time_zone, :mapping => %w( time_zone time_zone ) can be written without the mapping part (it's now assumed) - -* Added MacroReflection#macro which will return a symbol describing the macro used (like :composed_of or :has_many) #718, #248 *james@slashetc.com* - - -## 1.7.0 (24th February, 2005) ## - -* Changed the auto-timestamping feature to use ActiveRecord::Base.default_timezone instead of entertaining the parallel ActiveRecord::Base.timestamps_gmt method. The latter is now deprecated and will throw a warning on use (but still work) #710 *Jamis Buck* - -* Added a OCI8-based Oracle adapter that has been verified to work with Oracle 8 and 9 #629 [Graham Jenkins]. Usage notes: - - 1. Key generation uses a sequence "rails_sequence" for all tables. (I couldn't find a simple - and safe way of passing table-specific sequence information to the adapter.) - 2. Oracle uses DATE or TIMESTAMP datatypes for both dates and times. Consequently I have had to - resort to some hacks to get data converted to Date or Time in Ruby. - If the column_name ends in _at (like created_at, updated_at) it's created as a Ruby Time. Else if the - hours/minutes/seconds are 0, I make it a Ruby Date. Else it's a Ruby Time. - This is nasty - but if you use Duck Typing you'll probably not care very much. - In 9i it's tempting to map DATE to Date and TIMESTAMP to Time but I don't think that is - valid - too many databases use DATE for both. - Timezones and sub-second precision on timestamps are not supported. - 3. Default values that are functions (such as "SYSDATE") are not supported. This is a - restriction of the way active record supports default values. - 4. Referential integrity constraints are not fully supported. Under at least - some circumstances, active record appears to delete parent and child records out of - sequence and out of transaction scope. (Or this may just be a problem of test setup.) - - The OCI8 driver can be retrieved from http://rubyforge.org/projects/ruby-oci8/ - -* Added option :schema_order to the PostgreSQL adapter to support the use of multiple schemas per database #697 *YuriSchimke* - -* Optimized the SQL used to generate has_and_belongs_to_many queries by listing the join table first #693 *yerejm* - -* Fixed that when using validation macros with a custom message, if you happened to use single quotes in the message string you would get a parsing error #657 *tonka* - -* Fixed that Active Record would throw Broken Pipe errors with FCGI when the MySQL connection timed out instead of reconnecting #428 *Nicholas Seckar* - -* Added options to specify an SSL connection for MySQL. Define the following attributes in the connection config (config/database.yml in Rails) to use it: sslkey, sslcert, sslca, sslcapath, sslcipher. To use SSL with no client certs, just set :sslca = '/dev/null'. http://dev.mysql.com/doc/mysql/en/secure-connections.html #604 *daniel@nightrunner.com* - -* Added automatic dropping/creating of test tables for running the unit tests on all databases #587 *adelle@bullet.net.au* - -* Fixed that find_by_* would fail when column names had numbers #670 *demetrius* - -* Fixed the SQL Server adapter on a bunch of issues #667 *DeLynn* - - 1. Created a new columns method that is much cleaner. - 2. Corrected a problem with the select and select_all methods - that didn't account for the LIMIT clause being passed into raw SQL statements. - 3. Implemented the string_to_time method in order to create proper instances of the time class. - 4. Added logic to the simplified_type method that allows the database to specify the scale of float data. - 5. Adjusted the quote_column_name to account for the fact that MS SQL is bothered by a forward slash in the data string. - -* Fixed that the dynamic finder like find_all_by_something_boolean(false) didn't work #649 *lmarlow* - -* Added validates_each that validates each specified attribute against a block #610 [Jeremy Kemper]. Example: - - class Person < ActiveRecord::Base - validates_each :first_name, :last_name do |record, attr| - record.errors.add attr, 'starts with z.' if attr[0] == ?z - end - end - -* Added :allow_nil as an explicit option for validates_length_of, so unless that's set to true having the attribute as nil will also return an error if a range is specified as :within #610 *Jeremy Kemper* - -* Added that validates_* now accept blocks to perform validations #618 [Tim Bates]. Example: - - class Person < ActiveRecord::Base - validate { |person| person.errors.add("title", "will never be valid") if SHOULD_NEVER_BE_VALID } - end - -* Addded validation for validate all the associated objects before declaring failure with validates_associated #618 *Tim Bates* - -* Added keyword-style approach to defining the custom relational bindings #545 [Jamis Buck]. Example: - - class Project < ActiveRecord::Base - primary_key "sysid" - table_name "XYZ_PROJECT" - inheritance_column { original_inheritance_column + "_id" } - end - -* Fixed Base#clone for use with PostgreSQL #565 *hanson@surgery.wisc.edu* - - -## 1.6.0 (January 25th, 2005) ## - -* Added that has_many association build and create methods can take arrays of record data like Base#create and Base#build to build/create multiple records at once. - -* Added that Base#delete and Base#destroy both can take an array of ids to delete/destroy #336 - -* Added the option of supplying an array of attributes to Base#create, so that multiple records can be created at once. - -* Added the option of supplying an array of ids and attributes to Base#update, so that multiple records can be updated at once (inspired by #526/Duane Johnson). Example - - people = { 1 => { "first_name" => "David" }, 2 => { "first_name" => "Jeremy"} } - Person.update(people.keys, people.values) - -* Added ActiveRecord::Base.timestamps_gmt that can be set to true to make the automated timestamping use GMT instead of local time #520 *Scott Baron* - -* Added that update_all calls sanitize_sql on its updates argument, so stuff like MyRecord.update_all(['time = ?', Time.now]) works #519 *notahat* - -* Fixed that the dynamic finders didn't treat nil as a "IS NULL" but rather "= NULL" case #515 *Demetrius* - -* Added bind-named arrays for interpolating a group of ids or strings in conditions #528 *Jeremy Kemper* - -* Added that has_and_belongs_to_many associations with additional attributes also can be created between unsaved objects and only committed to the database when Base#save is called on the associator #524 *Eric Anderson* - -* Fixed that records fetched with piggy-back attributes or through rich has_and_belongs_to_many associations couldn't be saved due to the extra attributes not part of the table #522 *Eric Anderson* - -* Added mass-assignment protection for the inheritance column -- regardless of a custom column is used or not - -* Fixed that association proxies would fail === tests like PremiumSubscription === @account.subscription - -* Fixed that column aliases didn't work as expected with the new MySql411 driver #507 *Demetrius* - -* Fixed that find_all would produce invalid sql when called sequentialy #490 *Scott Baron* - - -## 1.5.1 (January 18th, 2005) ## - -* Fixed that the belongs_to and has_one proxy would fail a test like 'if project.manager' -- this unfortunately also means that you can't call methods like project.manager.build unless there already is a manager on the project #492 *Tim Bates* - -* Fixed that the Ruby/MySQL adapter wouldn't connect if the password was empty #503 *Pelle* - - -## 1.5.0 (January 17th, 2005) ## - -* Fixed that unit tests for MySQL are now run as the "rails" user instead of root #455 *Eric Hodel* - -* Added validates_associated that enables validation of objects in an unsaved association #398 [Tim Bates]. Example: - - class Book < ActiveRecord::Base - has_many :pages - belongs_to :library - - validates_associated :pages, :library - end - -* Added support for associating unsaved objects #402 [Tim Bates]. Rules that govern this addition: - - == Unsaved objects and associations - - You can manipulate objects and associations before they are saved to the database, but there is some special behaviour you should be - aware of, mostly involving the saving of associated objects. - - === One-to-one associations - - * Assigning an object to a has_one association automatically saves that object, and the object being replaced (if there is one), in - order to update their primary keys - except if the parent object is unsaved (new_record? == true). - * If either of these saves fail (due to one of the objects being invalid) the assignment statement returns false and the assignment - is cancelled. - * If you wish to assign an object to a has_one association without saving it, use the #association.build method (documented below). - * Assigning an object to a belongs_to association does not save the object, since the foreign key field belongs on the parent. It does - not save the parent either. - - === Collections - - * Adding an object to a collection (has_many or has_and_belongs_to_many) automatically saves that object, except if the parent object - (the owner of the collection) is not yet stored in the database. - * If saving any of the objects being added to a collection (via #push or similar) fails, then #push returns false. - * You can add an object to a collection without automatically saving it by using the #collection.build method (documented below). - * All unsaved (new_record? == true) members of the collection are automatically saved when the parent is saved. - -* Added replace to associations, so you can do project.manager.replace(new_manager) or project.milestones.replace(new_milestones) #402 *Tim Bates* - -* Added build and create methods to has_one and belongs_to associations, so you can now do project.manager.build(attributes) #402 *Tim Bates* - -* Added that if a before_* callback returns false, all the later callbacks and the associated action are cancelled. If an after_* callback returns false, all the later callbacks are cancelled. Callbacks are generally run in the order they are defined, with the exception of callbacks defined as methods on the model, which are called last. #402 *Tim Bates* - -* Fixed that Base#== wouldn't work for multiple references to the same unsaved object #402 *Tim Bates* - -* Fixed binary support for PostgreSQL #444 *alex@byzantine.no* - -* Added a differenciation between AssociationCollection#size and -length. Now AssociationCollection#size returns the size of the - collection by executing a SELECT COUNT(*) query if the collection hasn't been loaded and calling collection.size if it has. If - it's more likely than not that the collection does have a size larger than zero and you need to fetch that collection afterwards, - it'll take one less SELECT query if you use length. - -* Added Base#attributes that returns a hash of all the attributes with their names as keys and clones of their objects as values #433 *atyp.de* - -* Fixed that foreign keys named the same as the association would cause stack overflow #437 *Eric Anderson* - -* Fixed default scope of acts_as_list from "1" to "1 = 1", so it'll work in PostgreSQL (among other places) #427 *Alexey* - -* Added Base#reload that reloads the attributes of an object from the database #422 *Andreas Schwarz* - -* Added SQLite3 compatibility through the sqlite3-ruby adapter by Jamis Buck #381 *Jeremy Kemper* - -* Added support for the new protocol spoken by MySQL 4.1.1+ servers for the Ruby/MySQL adapter that ships with Rails #440 *Matt Mower* - -* Added that Observers can use the observes class method instead of overwriting self.observed_class(). - - Before: - class ListSweeper < ActiveRecord::Base - def self.observed_class() [ List, Item ] - end - - After: - class ListSweeper < ActiveRecord::Base - observes List, Item - end - -* Fixed that conditions in has_many and has_and_belongs_to_many should be interpolated just like the finder_sql is - -* Fixed Base#update_attribute to be indifferent to whether a string or symbol is used to describe the name - -* Added Base#toggle(attribute) and Base#toggle!(attribute) that makes it easier to flip a switch or flag. - - Before: topic.update_attribute(:approved, !approved?) - After : topic.toggle!(:approved) - -* Added Base#increment!(attribute) and Base#decrement!(attribute) that also saves the records. Example: - - page.views # => 1 - page.increment!(:views) # executes an UPDATE statement - page.views # => 2 - - page.increment(:views).increment!(:views) - page.views # => 4 - -* Added Base#increment(attribute) and Base#decrement(attribute) that encapsulates the += 1 and -= 1 patterns. - - - - -## 1.14.2 (April 9th, 2005) ## - -* Fixed calculations for the Oracle Adapter (closes #4626) *Michael Schoen* - - -## 1.14.1 (April 6th, 2006) ## - -* Fix type_name_with_module to handle type names that begin with '::'. Closes #4614. *Nicholas Seckar* - -* Fixed that that multiparameter assignment doesn't work with aggregations (closes #4620) *Lars Pind* - -* Enable Limit/Offset in Calculations (closes #4558) *lmarlow* - -* Fixed that loading including associations returns all results if Load IDs For Limited Eager Loading returns none (closes #4528) *Rick Olson* - -* Fixed HasManyAssociation#find bugs when :finder_sql is set #4600 *lagroue@free.fr* - -* Allow AR::Base#respond_to? to behave when @attributes is nil *Ryan Davis* - -* Support eager includes when going through a polymorphic has_many association. *Rick Olson* - -* Added support for eagerly including polymorphic has_one associations. (closes #4525) *Rick Olson* - - class Post < ActiveRecord::Base - has_one :tagging, :as => :taggable - end - - Post.find :all, :include => :tagging - -* Added descriptive error messages for invalid has_many :through associations: going through :has_one or :has_and_belongs_to_many *Rick Olson* - -* Added support for going through a polymorphic has_many association: (closes #4401) *Rick Olson* - - class PhotoCollection < ActiveRecord::Base - has_many :photos, :as => :photographic - belongs_to :firm - end - - class Firm < ActiveRecord::Base - has_many :photo_collections - has_many :photos, :through => :photo_collections - end - -* Multiple fixes and optimizations in PostgreSQL adapter, allowing ruby-postgres gem to work properly. *ruben.nine@gmail.com* - -* Fixed that AssociationCollection#delete_all should work even if the records of the association are not loaded yet. *Florian Weber* - -* Changed those private ActiveRecord methods to take optional third argument :auto instead of nil for performance optimizations. (closes #4456) *Stefan* - -* Private ActiveRecord methods add_limit!, add_joins!, and add_conditions! take an OPTIONAL third argument 'scope' (closes #4456) *Rick Olson* - -* DEPRECATED: Using additional attributes on has_and_belongs_to_many associations. Instead upgrade your association to be a real join model *David Heinemeier Hansson* - -* Fixed that records returned from has_and_belongs_to_many associations with additional attributes should be marked as read only (fixes #4512) *David Heinemeier Hansson* - -* Do not implicitly mark recordss of has_many :through as readonly but do mark habtm records as readonly (eventually only on join tables without rich attributes). *Marcel Mollina Jr.* - -* Fixed broken OCIAdapter #4457 *Michael Schoen* - - -## 1.14.0 (March 27th, 2006) ## - -* Replace 'rescue Object' with a finer grained rescue. Closes #4431. *Nicholas Seckar* - -* Fixed eager loading so that an aliased table cannot clash with a has_and_belongs_to_many join table *Rick Olson* - -* Add support for :include to with_scope *andrew@redlinesoftware.com* - -* Support the use of public synonyms with the Oracle adapter; required ruby-oci8 v0.1.14 #4390 *Michael Schoen* - -* Change periods (.) in table aliases to _'s. Closes #4251 *jeff@ministrycentered.com* - -* Changed has_and_belongs_to_many join to INNER JOIN for Mysql 3.23.x. Closes #4348 *Rick Olson* - -* Fixed issue that kept :select options from being scoped *Rick Olson* - -* Fixed db_schema_import when binary types are present #3101 *David Heinemeier Hansson* - -* Fixed that MySQL enums should always be returned as strings #3501 *David Heinemeier Hansson* - -* Change has_many :through to use the :source option to specify the source association. :class_name is now ignored. *Rick Olson* - - class Connection < ActiveRecord::Base - belongs_to :user - belongs_to :channel - end - - class Channel < ActiveRecord::Base - has_many :connections - has_many :contacts, :through => :connections, :class_name => 'User' # OLD - has_many :contacts, :through => :connections, :source => :user # NEW - end - -* Fixed DB2 adapter so nullable columns will be determines correctly now and quotes from column default values will be removed #4350 *contact@maik-schmidt.de* - -* Allow overriding of find parameters in scoped has_many :through calls *Rick Olson* - - In this example, :include => false disables the default eager association from loading. :select changes the standard - select clause. :joins specifies a join that is added to the end of the has_many :through query. - - class Post < ActiveRecord::Base - has_many :tags, :through => :taggings, :include => :tagging do - def add_joins_and_select - find :all, :select => 'tags.*, authors.id as author_id', :include => false, - :joins => 'left outer join posts on taggings.taggable_id = posts.id left outer join authors on posts.author_id = authors.id' - end - end - end - -* Fixed that schema changes while the database was open would break any connections to an SQLite database (now we reconnect if that error is throw) *David Heinemeier Hansson* - -* Don't classify the has_one class when eager loading, it is already singular. Add tests. (closes #4117) *Jonathan Viney* - -* Quit ignoring default :include options in has_many :through calls *Mark James* - -* Allow has_many :through associations to find the source association by setting a custom class (closes #4307) *Jonathan Viney* - -* Eager Loading support added for has_many :through => :has_many associations (see below). *Rick Olson* - -* Allow has_many :through to work on has_many associations (closes #3864) [sco@scottraymond.net] Example: - - class Firm < ActiveRecord::Base - has_many :clients - has_many :invoices, :through => :clients - end - - class Client < ActiveRecord::Base - belongs_to :firm - has_many :invoices - end - - class Invoice < ActiveRecord::Base - belongs_to :client - end - -* Raise error when trying to select many polymorphic objects with has_many :through or :include (closes #4226) *Josh Susser* - -* Fixed has_many :through to include :conditions set on the :through association. closes #4020 *Jonathan Viney* - -* Fix that has_many :through honors the foreign key set by the belongs_to association in the join model (closes #4259) *andylien@gmail.com / Rick Olson* - -* SQL Server adapter gets some love #4298 *Ryan Tomayko* - -* Added OpenBase database adapter that builds on top of the http://www.spice-of-life.net/ruby-openbase/ driver. All functionality except LIMIT/OFFSET is supported #3528 *derrickspell@cdmplus.com* - -* Rework table aliasing to account for truncated table aliases. Add smarter table aliasing when doing eager loading of STI associations. This allows you to use the association name in the order/where clause. [Jonathan Viney / Rick Olson] #4108 Example (SpecialComment is using STI): - - Author.find(:all, :include => { :posts => :special_comments }, :order => 'special_comments.body') - -* Add AbstractAdapter#table_alias_for to create table aliases according to the rules of the current adapter. *Rick Olson* - -* Provide access to the underlying database connection through Adapter#raw_connection. Enables the use of db-specific methods without complicating the adapters. #2090 *Michael Koziarski* - -* Remove broken attempts at handling columns with a default of 'now()' in the postgresql adapter. #2257 *Michael Koziarski* - -* Added connection#current_database that'll return of the current database (only works in MySQL, SQL Server, and Oracle so far -- please help implement for the rest of the adapters) #3663 *Tom Ward* - -* Fixed that Migration#execute would have the table name prefix appended to its query #4110 *mark.imbriaco@pobox.com* - -* Make all tinyint(1) variants act like boolean in mysql (tinyint(1) unsigned, etc.) *Jamis Buck* - -* Use association's :conditions when eager loading. [Jeremy Evans] #4144 - -* Alias the has_and_belongs_to_many join table on eager includes. #4106 *Jeremy Evans* - - This statement would normally error because the projects_developers table is joined twice, and therefore joined_on would be ambiguous. - - Developer.find(:all, :include => {:projects => :developers}, :conditions => 'join_project_developers.joined_on IS NOT NULL') - -* Oracle adapter gets some love #4230 *Michael Schoen* - - * Changes :text to CLOB rather than BLOB [Moses Hohman] - * Fixes an issue with nil numeric length/scales (several) - * Implements support for XMLTYPE columns [wilig / Kubo Takehiro] - * Tweaks a unit test to get it all green again - * Adds support for #current_database - -* Added Base.abstract_class? that marks which classes are not part of the Active Record hierarchy #3704 *Rick Olson* - - class CachedModel < ActiveRecord::Base - self.abstract_class = true - end - - class Post < CachedModel - end - - CachedModel.abstract_class? - => true - - Post.abstract_class? - => false - - Post.base_class - => Post - - Post.table_name - => 'posts' - -* Allow :dependent options to be used with polymorphic joins. #3820 *Rick Olson* - - class Foo < ActiveRecord::Base - has_many :attachments, :as => :attachable, :dependent => :delete_all - end - -* Nicer error message on has_many :through when :through reflection can not be found. #4042 *court3nay* - -* Upgrade to Transaction::Simple 1.3 *Jamis Buck* - -* Catch FixtureClassNotFound when using instantiated fixtures on a fixture that has no ActiveRecord model *Rick Olson* - -* Allow ordering of calculated results and/or grouped fields in calculations *solo@gatelys.com* - -* Make ActiveRecord::Base#save! return true instead of nil on success. #4173 *johan@johansorensen.com* - -* Dynamically set allow_concurrency. #4044 *Stefan Kaes* - -* Added Base#to_xml that'll turn the current record into a XML representation [David Heinemeier Hansson]. Example: - - topic.to_xml - - ...returns: - - <?xml version="1.0" encoding="UTF-8"?> - <topic> - <title>The First Topic</title> - <author-name>David</author-name> - <id type="integer">1</id> - <approved type="boolean">false</approved> - <replies-count type="integer">0</replies-count> - <bonus-time type="datetime">2000-01-01 08:28:00</bonus-time> - <written-on type="datetime">2003-07-16 09:28:00</written-on> - <content>Have a nice day</content> - <author-email-address>david@loudthinking.com</author-email-address> - <parent-id></parent-id> - <last-read type="date">2004-04-15</last-read> - </topic> - - ...and you can configure with: - - topic.to_xml(:skip_instruct => true, :except => [ :id, bonus_time, :written_on, replies_count ]) - - ...that'll return: - - <topic> - <title>The First Topic</title> - <author-name>David</author-name> - <approved type="boolean">false</approved> - <content>Have a nice day</content> - <author-email-address>david@loudthinking.com</author-email-address> - <parent-id></parent-id> - <last-read type="date">2004-04-15</last-read> - </topic> - - You can even do load first-level associations as part of the document: - - firm.to_xml :include => [ :account, :clients ] - - ...that'll return something like: - - <?xml version="1.0" encoding="UTF-8"?> - <firm> - <id type="integer">1</id> - <rating type="integer">1</rating> - <name>37signals</name> - <clients> - <client> - <rating type="integer">1</rating> - <name>Summit</name> - </client> - <client> - <rating type="integer">1</rating> - <name>Microsoft</name> - </client> - </clients> - <account> - <id type="integer">1</id> - <credit-limit type="integer">50</credit-limit> - </account> - </firm> - -* Allow :counter_cache to take a column name for custom counter cache columns *Jamis Buck* - -* Documentation fixes for :dependent *robby@planetargon.com* - -* Stop the MySQL adapter crashing when views are present. #3782 *Jonathan Viney* - -* Don't classify the belongs_to class, it is already singular #4117 *keithm@infused.org* - -* Allow set_fixture_class to take Classes instead of strings for a class in a module. Raise FixtureClassNotFound if a fixture can't load. *Rick Olson* - -* Fix quoting of inheritance column for STI eager loading #4098 *Jonathan Viney <jonathan@bluewire.net.nz>* - -* Added smarter table aliasing for eager associations for multiple self joins #3580 *Rick Olson* - - * The first time a table is referenced in a join, no alias is used. - * After that, the parent class name and the reflection name are used. - - Tree.find(:all, :include => :children) # LEFT OUTER JOIN trees AS tree_children ... - - * Any additional join references get a numerical suffix like '_2', '_3', etc. - -* Fixed eager loading problems with single-table inheritance #3580 [Rick Olson]. Post.find(:all, :include => :special_comments) now returns all posts, and any special comments that the posts may have. And made STI work with has_many :through and polymorphic belongs_to. - -* Added cascading eager loading that allows for queries like Author.find(:all, :include=> { :posts=> :comments }), which will fetch all authors, their posts, and the comments belonging to those posts in a single query (using LEFT OUTER JOIN) #3913 [anna@wota.jp]. Examples: - - # cascaded in two levels - >> Author.find(:all, :include=>{:posts=>:comments}) - => authors - +- posts - +- comments - - # cascaded in two levels and normal association - >> Author.find(:all, :include=>[{:posts=>:comments}, :categorizations]) - => authors - +- posts - +- comments - +- categorizations - - # cascaded in two levels with two has_many associations - >> Author.find(:all, :include=>{:posts=>[:comments, :categorizations]}) - => authors - +- posts - +- comments - +- categorizations - - # cascaded in three levels - >> Company.find(:all, :include=>{:groups=>{:members=>{:favorites}}}) - => companies - +- groups - +- members - +- favorites - -* Make counter cache work when replacing an association #3245 *eugenol@gmail.com* - -* Make migrations verbose *Jamis Buck* - -* Make counter_cache work with polymorphic belongs_to *Jamis Buck* - -* Fixed that calling HasOneProxy#build_model repeatedly would cause saving to happen #4058 *anna@wota.jp* - -* Added Sybase database adapter that relies on the Sybase Open Client bindings (see http://raa.ruby-lang.org/project/sybase-ctlib) #3765 [John Sheets]. It's almost completely Active Record compliant (including migrations), but has the following caveats: - - * Does not support DATE SQL column types; use DATETIME instead. - * Date columns on HABTM join tables are returned as String, not Time. - * Insertions are potentially broken for :polymorphic join tables - * BLOB column access not yet fully supported - -* Clear stale, cached connections left behind by defunct threads. *Jeremy Kemper* - -* CHANGED DEFAULT: set ActiveRecord::Base.allow_concurrency to false. Most AR usage is in single-threaded applications. *Jeremy Kemper* - -* Renamed the "oci" adapter to "oracle", but kept the old name as an alias #4017 *Michael Schoen* - -* Fixed that Base.save should always return false if the save didn't succeed, including if it has halted by before_save's #1861, #2477 *David Heinemeier Hansson* - -* Speed up class -> connection caching and stale connection verification. #3979 *Stefan Kaes* - -* Add set_fixture_class to allow the use of table name accessors with models which use set_table_name. *Kevin Clark* - -* Added that fixtures to placed in subdirectories of the main fixture files are also loaded #3937 *dblack@wobblini.net* - -* Define attribute query methods to avoid method_missing calls. #3677 *Jonathan Viney* - -* ActiveRecord::Base.remove_connection explicitly closes database connections and doesn't corrupt the connection cache. Introducing the disconnect! instance method for the PostgreSQL, MySQL, and SQL Server adapters; implementations for the others are welcome. #3591 *Simon Stapleton, Tom Ward* - -* Added support for nested scopes #3407 [anna@wota.jp]. Examples: - - Developer.with_scope(:find => { :conditions => "salary > 10000", :limit => 10 }) do - Developer.find(:all) # => SELECT * FROM developers WHERE (salary > 10000) LIMIT 10 - - # inner rule is used. (all previous parameters are ignored) - Developer.with_exclusive_scope(:find => { :conditions => "name = 'Jamis'" }) do - Developer.find(:all) # => SELECT * FROM developers WHERE (name = 'Jamis') - end - - # parameters are merged - Developer.with_scope(:find => { :conditions => "name = 'Jamis'" }) do - Developer.find(:all) # => SELECT * FROM developers WHERE (( salary > 10000 ) AND ( name = 'Jamis' )) LIMIT 10 - end - end - -* Fixed db2 connection with empty user_name and auth options #3622 *phurley@gmail.com* - -* Fixed validates_length_of to work on UTF-8 strings by using characters instead of bytes #3699 *Masao Mutoh* - -* Fixed that reflections would bleed across class boundaries in single-table inheritance setups #3796 *Lars Pind* - -* Added calculations: Base.count, Base.average, Base.sum, Base.minimum, Base.maxmium, and the generic Base.calculate. All can be used with :group and :having. Calculations and statitics need no longer require custom SQL. #3958 [Rick Olson]. Examples: - - Person.average :age - Person.minimum :age - Person.maximum :age - Person.sum :salary, :group => :last_name - -* Renamed Errors#count to Errors#size but kept an alias for the old name (and included an alias for length too) #3920 *Luke Redpath* - -* Reflections don't attempt to resolve module nesting of association classes. Simplify type computation. *Jeremy Kemper* - -* Improved the Oracle OCI Adapter with better performance for column reflection (from #3210), fixes to migrations (from #3476 and #3742), tweaks to unit tests (from #3610), and improved documentation (from #2446) #3879 *Aggregated by schoenm@earthlink.net* - -* Fixed that the schema_info table used by ActiveRecord::Schema.define should respect table pre- and suffixes #3834 *rubyonrails@atyp.de* - -* Added :select option to Base.count that'll allow you to select something else than * to be counted on. Especially important for count queries using DISTINCT #3839 *Stefan Kaes* - -* Correct syntax error in mysql DDL, and make AAACreateTablesTest run first *Bob Silva* - -* Allow :include to be used with has_many :through associations #3611 *Michael Schoen* - -* PostgreSQL: smarter schema dumps using pk_and_sequence_for(table). #2920 *Blair Zajac* - -* SQLServer: more compatible limit/offset emulation. #3779 *Tom Ward* - -* Polymorphic join support for has_one associations (has_one :foo, :as => :bar) #3785 *Rick Olson* - -* PostgreSQL: correctly parse negative integer column defaults. #3776 *bellis@deepthought.org* - -* Fix problems with count when used with :include *Jeremy Hopple and Kevin Clark* - -* ActiveRecord::RecordInvalid now states which validations failed in its default error message *Tobias Lütke* - -* Using AssociationCollection#build with arrays of hashes should call build, not create *David Heinemeier Hansson* - -* Remove definition of reloadable? from ActiveRecord::Base to make way for new Reloadable code. *Nicholas Seckar* - -* Fixed schema handling for DB2 adapter that didn't work: an initial schema could be set, but it wasn't used when getting tables and indexes #3678 *Maik Schmidt* - -* Support the :column option for remove_index with the PostgreSQL adapter. #3661 *Shugo Maeda* - -* Add documentation for add_index and remove_index. #3600 *Manfred Stienstra <m.stienstra@fngtps.com>* - -* If the OCI library is not available, raise an exception indicating as much. #3593 *Michael Schoen* - -* Add explicit :order in finder tests as postgresql orders results differently by default. #3577. *Rick Olson* - -* Make dynamic finders honor additional passed in :conditions. #3569 *Oleg Pudeyev <pudeyo@rpi.edu>, Marcel Molina Jr.* - -* Show a meaningful error when the DB2 adapter cannot be loaded due to missing dependencies. *Nicholas Seckar* - -* Make .count work for has_many associations with multi line finder sql *Michael Schoen* - -* Add AR::Base.base_class for querying the ancestor AR::Base subclass *Jamis Buck* - -* Allow configuration of the column used for optimistic locking *wilsonb@gmail.com* - -* Don't hardcode 'id' in acts as list. *ror@philippeapril.com* - -* Fix date errors for SQLServer in association tests. #3406 *Kevin Clark* - -* Escape database name in MySQL adapter when creating and dropping databases. #3409 *anna@wota.jp* - -* Disambiguate table names for columns in validates_uniqueness_of's WHERE clause. #3423 *alex.borovsky@gmail.com* - -* .with_scope imposed create parameters now bypass attr_protected *Tobias Lütke* - -* Don't raise an exception when there are more keys than there are named bind variables when sanitizing conditions. *Marcel Molina Jr.* - -* Multiple enhancements and adjustments to DB2 adaptor. #3377 *contact@maik-schmidt.de* - -* Sanitize scoped conditions. *Marcel Molina Jr.* - -* Added option to Base.reflection_of_all_associations to specify a specific association to scope the call. For example Base.reflection_of_all_associations(:has_many) *David Heinemeier Hansson* - -* Added ActiveRecord::SchemaDumper.ignore_tables which tells SchemaDumper which tables to ignore. Useful for tables with funky column like the ones required for tsearch2. *Tobias Lütke* - -* SchemaDumper now doesn't fail anymore when there are unknown column types in the schema. Instead the table is ignored and a Comment is left in the schema.rb. *Tobias Lütke* - -* Fixed that saving a model with multiple habtm associations would only save the first one. #3244 *yanowitz-rubyonrails@quantumfoam.org, Florian Weber* - -* Fix change_column to work with PostgreSQL 7.x and 8.x. #3141 *wejn@box.cz, Rick Olson, Scott Barron* - -* removed :piggyback in favor of just allowing :select on :through associations. *Tobias Lütke* - -* made method missing delegation to class methods on relation target work on :through associations. *Tobias Lütke* - -* made .find() work on :through relations. *Tobias Lütke* - -* Fix typo in association docs. #3296. *Blair Zajac* - -* Fixed :through relations when using STI inherited classes would use the inherited class's name as foreign key on the join model *Tobias Lütke* - -## 1.13.2 (December 13th, 2005) ## - -* Become part of Rails 1.0 - -* MySQL: allow encoding option for mysql.rb driver. *Jeremy Kemper* - -* Added option inheritance for find calls on has_and_belongs_to_many and has_many assosociations [David Heinemeier Hansson]. Example: - - class Post - has_many :recent_comments, :class_name => "Comment", :limit => 10, :include => :author - end - - post.recent_comments.find(:all) # Uses LIMIT 10 and includes authors - post.recent_comments.find(:all, :limit => nil) # Uses no limit but include authors - post.recent_comments.find(:all, :limit => nil, :include => nil) # Uses no limit and doesn't include authors - -* Added option to specify :group, :limit, :offset, and :select options from find on has_and_belongs_to_many and has_many assosociations *David Heinemeier Hansson* - -* MySQL: fixes for the bundled mysql.rb driver. #3160 *Justin Forder* - -* SQLServer: fix obscure optimistic locking bug. #3068 *kajism@yahoo.com* - -* SQLServer: support uniqueidentifier columns. #2930 *keithm@infused.org* - -* SQLServer: cope with tables names qualified by owner. #3067 *jeff@ministrycentered.com* - -* SQLServer: cope with columns with "desc" in the name. #1950 *Ron Lusk, Ryan Tomayko* - -* SQLServer: cope with primary keys with "select" in the name. #3057 *rdifrango@captechventures.com* - -* Oracle: active? performs a select instead of a commit. #3133 *Michael Schoen* - -* MySQL: more robust test for nullified result hashes. #3124 *Stefan Kaes* - -* Reloading an instance refreshes its aggregations as well as its associations. #3024 *François Beausoleil* - -* Fixed that using :include together with :conditions array in Base.find would cause NoMethodError #2887 *Paul Hammmond* - -* PostgreSQL: more robust sequence name discovery. #3087 *Rick Olson* - -* Oracle: use syntax compatible with Oracle 8. #3131 *Michael Schoen* - -* MySQL: work around ruby-mysql/mysql-ruby inconsistency with mysql.stat. Eliminate usage of mysql.ping because it doesn't guarantee reconnect. Explicitly close and reopen the connection instead. *Jeremy Kemper* - -* Added preliminary support for polymorphic associations *David Heinemeier Hansson* - -* Added preliminary support for join models *David Heinemeier Hansson* - -* Allow validate_uniqueness_of to be scoped by more than just one column. #1559. *jeremy@jthopple.com, Marcel Molina Jr.* - -* Firebird: active? and reconnect! methods for handling stale connections. #428 *Ken Kunz <kennethkunz@gmail.com>* - -* Firebird: updated for FireRuby 0.4.0. #3009 *Ken Kunz <kennethkunz@gmail.com>* - -* MySQL and PostgreSQL: active? compatibility with the pure-Ruby driver. #428 *Jeremy Kemper* - -* Oracle: active? check pings the database rather than testing the last command status. #428 *Michael Schoen* - -* SQLServer: resolve column aliasing/quoting collision when using limit or offset in an eager find. #2974 *kajism@yahoo.com* - -* Reloading a model doesn't lose track of its connection. #2996 *junk@miriamtech.com, Jeremy Kemper* - -* Fixed bug where using update_attribute after pushing a record to a habtm association of the object caused duplicate rows in the join table. #2888 *colman@rominato.com, Florian Weber, Michael Schoen* - -* MySQL, PostgreSQL: reconnect! also reconfigures the connection. Otherwise, the connection 'loses' its settings if it times out and is reconnected. #2978 *Shugo Maeda* - -* has_and_belongs_to_many: use JOIN instead of LEFT JOIN. *Jeremy Kemper* - -* MySQL: introduce :encoding option to specify the character set for client, connection, and results. Only available for MySQL 4.1 and later with the mysql-ruby driver. Do SHOW CHARACTER SET in mysql client to see available encodings. #2975 *Shugo Maeda* - -* Add tasks to create, drop and rebuild the MySQL and PostgreSQL test databases. *Marcel Molina Jr.* - -* Correct boolean handling in generated reader methods. #2945 *Don Park, Stefan Kaes* - -* Don't generate read methods for columns whose names are not valid ruby method names. #2946 *Stefan Kaes* - -* Document :force option to create_table. #2921 *Blair Zajac <blair@orcaware.com>* - -* Don't add the same conditions twice in has_one finder sql. #2916 *Jeremy Evans* - -* Rename Version constant to VERSION. #2802 *Marcel Molina Jr.* - -* Introducing the Firebird adapter. Quote columns and use attribute_condition more consistently. Setup guide: http://wiki.rubyonrails.com/rails/pages/Firebird+Adapter #1874 *Ken Kunz <kennethkunz@gmail.com>* - -* SQLServer: active? and reconnect! methods for handling stale connections. #428 *kajism@yahoo.com, Tom Ward <tom@popdog.net>* - -* Associations handle case-equality more consistently: item.parts.is_a?(Array) and item.parts === Array. #1345 *MarkusQ@reality.com* - -* SQLServer: insert uses given primary key value if not nil rather than SELECT @@IDENTITY. #2866 *kajism@yahoo.com, Tom Ward <tom@popdog.net>* - -* Oracle: active? and reconnect! methods for handling stale connections. Optionally retry queries after reconnect. #428 *Michael Schoen <schoenm@earthlink.net>* - -* Correct documentation for Base.delete_all. #1568 *Newhydra* - -* Oracle: test case for column default parsing. #2788 *Michael Schoen <schoenm@earthlink.net>* - -* Update documentation for Migrations. #2861 *Tom Werner <tom@cube6media.com>* - -* When AbstractAdapter#log rescues an exception, attempt to detect and reconnect to an inactive database connection. Connection adapter must respond to the active? and reconnect! instance methods. Initial support for PostgreSQL, MySQL, and SQLite. Make certain that all statements which may need reconnection are performed within a logged block: for example, this means no avoiding log(sql, name) { } if @logger.nil? #428 *Jeremy Kemper* - -* Oracle: Much faster column reflection. #2848 *Michael Schoen <schoenm@earthlink.net>* - -* Base.reset_sequence_name analogous to reset_table_name (mostly useful for testing). Base.define_attr_method allows nil values. *Jeremy Kemper* - -* PostgreSQL: smarter sequence name defaults, stricter last_insert_id, warn on pk without sequence. *Jeremy Kemper* - -* PostgreSQL: correctly discover custom primary key sequences. #2594 *Blair Zajac <blair@orcaware.com>, meadow.nnick@gmail.com, Jeremy Kemper* - -* SQLServer: don't report limits for unsupported field types. #2835 *Ryan Tomayko* - -* Include the Enumerable module in ActiveRecord::Errors. *Rick Bradley <rick@rickbradley.com>* - -* Add :group option, correspond to GROUP BY, to the find method and to the has_many association. #2818 *rubyonrails@atyp.de* - -* Don't cast nil or empty strings to a dummy date. #2789 *Rick Bradley <rick@rickbradley.com>* - -* acts_as_list plays nicely with inheritance by remembering the class which declared it. #2811 *rephorm@rephorm.com* - -* Fix sqlite adaptor's detection of missing dbfile or database declaration. *Nicholas Seckar* - -* Fixed acts_as_list for definitions without an explicit :order #2803 *Jonathan Viney* - -* Upgrade bundled ruby-mysql 0.2.4 with mysql411 shim (see #440) to ruby-mysql 0.2.6 with a patchset for 4.1 protocol support. Local change [301] is now a part of the main driver; reapplied local change [2182]. Removed GC.start from Result.free. *tommy@tmtm.org, akuroda@gmail.com, Doug Fales <doug.fales@gmail.com>, Jeremy Kemper* - -* Correct handling of complex order clauses with SQL Server limit emulation. #2770 *Tom Ward <tom@popdog.net>, Matt B.* - -* Correct whitespace problem in Oracle default column value parsing. #2788 *rick@rickbradley.com* - -* Destroy associated has_and_belongs_to_many records after all before_destroy callbacks but before destroy. This allows you to act on the habtm association as you please while preserving referential integrity. #2065 *larrywilliams1@gmail.com, sam.kirchmeier@gmail.com, elliot@townx.org, Jeremy Kemper* - -* Deprecate the old, confusing :exclusively_dependent option in favor of :dependent => :delete_all. *Jeremy Kemper* - -* More compatible Oracle column reflection. #2771 *Ryan Davis <ryand-ruby@zenspider.com>, Michael Schoen <schoenm@earthlink.net>* - - -## 1.13.0 (November 7th, 2005) ## - -* Fixed faulty regex in get_table_name method (SQLServerAdapter) #2639 *Ryan Tomayko* - -* Added :include as an option for association declarations [David Heinemeier Hansson]. Example: - - has_many :posts, :include => [ :author, :comments ] - -* Rename Base.constrain to Base.with_scope so it doesn't conflict with existing concept of database constraints. Make scoping more robust: uniform method => parameters, validated method names and supported finder parameters, raise exception on nested scopes. [Jeremy Kemper] Example: - - Comment.with_scope(:find => { :conditions => 'active=true' }, :create => { :post_id => 5 }) do - # Find where name = ? and active=true - Comment.find :all, :conditions => ['name = ?', name] - # Create comment associated with :post_id - Comment.create :body => "Hello world" - end - -* Fixed that SQL Server should ignore :size declarations on anything but integer and string in the agnostic schema representation #2756 *Ryan Tomayko* - -* Added constrain scoping for creates using a hash of attributes bound to the :creation key [David Heinemeier Hansson]. Example: - - Comment.constrain(:creation => { :post_id => 5 }) do - # Associated with :post_id - Comment.create :body => "Hello world" - end - - This is rarely used directly, but allows for find_or_create on associations. So you can do: - - # If the tag doesn't exist, a new one is created that's associated with the person - person.tags.find_or_create_by_name("Summer") - -* Added find_or_create_by_X as a second type of dynamic finder that'll create the record if it doesn't already exist [David Heinemeier Hansson]. Example: - - # No 'Summer' tag exists - Tag.find_or_create_by_name("Summer") # equal to Tag.create(:name => "Summer") - - # Now the 'Summer' tag does exist - Tag.find_or_create_by_name("Summer") # equal to Tag.find_by_name("Summer") - -* Added extension capabilities to has_many and has_and_belongs_to_many proxies [David Heinemeier Hansson]. Example: - - class Account < ActiveRecord::Base - has_many :people do - def find_or_create_by_name(name) - first_name, *last_name = name.split - last_name = last_name.join " " - - find_or_create_by_first_name_and_last_name(first_name, last_name) - end - end - end - - person = Account.find(:first).people.find_or_create_by_name("David Heinemeier Hansson") - person.first_name # => "David" - person.last_name # => "Heinemeier Hansson" - - Note that the anoymous module must be declared using brackets, not do/end (due to order of evaluation). - -* Omit internal dtproperties table from SQLServer table list. #2729 *Ryan Tomayko* - -* Quote column names in generated SQL. #2728 *Ryan Tomayko* - -* Correct the pure-Ruby MySQL 4.1.1 shim's version test. #2718 *Jeremy Kemper* - -* Add Model.create! to match existing model.save! method. When save! raises RecordInvalid, you can catch the exception, retrieve the invalid record (invalid_exception.record), and see its errors (invalid_exception.record.errors). *Jeremy Kemper* - -* Correct fixture behavior when table name pluralization is off. #2719 *Rick Bradley <rick@rickbradley.com>* - -* Changed :dbfile to :database for SQLite adapter for consistency (old key still works as an alias) #2644 *Dan Peterson* - -* Added migration support for Oracle #2647 *Michael Schoen* - -* Worked around that connection can't be reset if allow_concurrency is off. #2648 *Michael Schoen <schoenm@earthlink.net>* - -* Fixed SQL Server adapter to pass even more tests and do even better #2634 *Ryan Tomayko* - -* Fixed SQL Server adapter so it honors options[:conditions] when applying :limits #1978 *Tom Ward* - -* Added migration support to SQL Server adapter (please someone do the same for Oracle and DB2) #2625 *Tom Ward* - -* Use AR::Base.silence rather than AR::Base.logger.silence in fixtures to preserve Log4r compatibility. #2618 *dansketcher@gmail.com* - -* Constraints are cloned so they can't be inadvertently modified while they're - in effect. Added :readonly finder constraint. Calling an association collection's class method (Part.foobar via item.parts.foobar) constrains :readonly => false since the collection's :joins constraint would otherwise force it to true. [Jeremy Kemper <rails@bitsweat.net>] -* Added :offset and :limit to the kinds of options that Base.constrain can use #2466 *duane.johnson@gmail.com* - -* Fixed handling of nil number columns on Oracle and cleaned up tests for Oracle in general #2555 *Michael Schoen* - -* Added quoted_true and quoted_false methods and tables to db2_adapter and cleaned up tests for DB2 #2493, #2624 *maik schmidt* - - -## 1.12.2 (October 26th, 2005) ## - -* Allow symbols to rename columns when using SQLite adapter. #2531 *Kevin Clark* - -* Map Active Record time to SQL TIME. #2575, #2576 *Robby Russell <robby@planetargon.com>* - -* Clarify semantics of ActiveRecord::Base#respond_to? #2560 *Stefan Kaes* - -* Fixed Association#clear for associations which have not yet been accessed. #2524 *Patrick Lenz <patrick@lenz.sh>* - -* HABTM finders shouldn't return readonly records. #2525 *Patrick Lenz <patrick@lenz.sh>* - -* Make all tests runnable on their own. #2521. *Blair Zajac <blair@orcaware.com>* - - -## 1.12.1 (October 19th, 2005) ## - -* Always parenthesize :conditions options so they may be safely combined with STI and constraints. - -* Correct PostgreSQL primary key sequence detection. #2507 *tmornini@infomania.com* - -* Added support for using limits in eager loads that involve has_many and has_and_belongs_to_many associations - - -## 1.12.0 (October 16th, 2005) ## - -* Update/clean up documentation (rdoc) - -* PostgreSQL sequence support. Use set_sequence_name in your model class to specify its primary key sequence. #2292 *Rick Olson <technoweenie@gmail.com>, Robby Russell <robby@planetargon.com>* - -* Change default logging colors to work on both white and black backgrounds. *Sam Stephenson* - -* YAML fixtures support ordered hashes for fixtures with foreign key dependencies in the same table. #1896 *purestorm@ggnore.net* - -* :dependent now accepts :nullify option. Sets the foreign key of the related objects to NULL instead of deleting them. #2015 *Robby Russell <robby@planetargon.com>* - -* Introduce read-only records. If you call object.readonly! then it will mark the object as read-only and raise ReadOnlyRecord if you call object.save. object.readonly? reports whether the object is read-only. Passing :readonly => true to any finder method will mark returned records as read-only. The :joins option now implies :readonly, so if you use this option, saving the same record will now fail. Use find_by_sql to work around. - -* Avoid memleak in dev mode when using fcgi - -* Simplified .clear on active record associations by using the existing delete_records method. #1906 *Caleb <me@cpb.ca>* - -* Delegate access to a customized primary key to the conventional id method. #2444. *Blair Zajac <blair@orcaware.com>* - -* Fix errors caused by assigning a has-one or belongs-to property to itself - -* Add ActiveRecord::Base.schema_format setting which specifies how databases should be dumped *Sam Stephenson* - -* Update DB2 adapter. #2206. *contact@maik-schmidt.de* - -* Corrections to SQLServer native data types. #2267. *rails.20.clarry@spamgourmet.com* - -* Deprecated ActiveRecord::Base.threaded_connection in favor of ActiveRecord::Base.allow_concurrency. - -* Protect id attribute from mass assigment even when the primary key is set to something else. #2438. *Blair Zajac <blair@orcaware.com>* - -* Misc doc fixes (typos/grammar/etc.). #2430. *coffee2code* - -* Add test coverage for content_columns. #2432. *coffee2code* - -* Speed up for unthreaded environments. #2431. *Stefan Kaes* - -* Optimization for Mysql selects using mysql-ruby extension greater than 2.6.3. #2426. *Stefan Kaes* - -* Speed up the setting of table_name. #2428. *Stefan Kaes* - -* Optimize instantiation of STI subclass records. In partial fullfilment of #1236. *Stefan Kaes* - -* Fix typo of 'constrains' to 'contraints'. #2069. *Michael Schuerig <michael@schuerig.de>* - -* Optimization refactoring for add_limit_offset!. In partial fullfilment of #1236. *Stefan Kaes* - -* Add ability to get all siblings, including the current child, with acts_as_tree. Recloses #2140. *Michael Schuerig <michael@schuerig.de>* - -* Add geometric type for postgresql adapter. #2233 *Andrew Kaspick* - -* Add option (true by default) to generate reader methods for each attribute of a record to avoid the overhead of calling method missing. In partial fullfilment of #1236. *Stefan Kaes* - -* Add convenience predicate methods on Column class. In partial fullfilment of #1236. *Stefan Kaes* - -* Raise errors when invalid hash keys are passed to ActiveRecord::Base.find. #2363 *Chad Fowler <chad@chadfowler.com>, Nicholas Seckar* - -* Added :force option to create_table that'll try to drop the table if it already exists before creating - -* Fix transactions so that calling return while inside a transaction will not leave an open transaction on the connection. *Nicholas Seckar* - -* Use foreign_key inflection uniformly. #2156 *Blair Zajac <blair@orcaware.com>* - -* model.association.clear should destroy associated objects if :dependent => true instead of nullifying their foreign keys. #2221 *joergd@pobox.com, ObieFernandez <obiefernandez@gmail.com>* - -* Returning false from before_destroy should cancel the action. #1829 *Jeremy Huffman* - -* Recognize PostgreSQL NOW() default as equivalent to CURRENT_TIMESTAMP or CURRENT_DATE, depending on the column's type. #2256 *mat <mat@absolight.fr>* - -* Extensive documentation for the abstract database adapter. #2250 *François Beausoleil <fbeausoleil@ftml.net>* - -* Clean up Fixtures.reset_sequences for PostgreSQL. Handle tables with no rows and models with custom primary keys. #2174, #2183 *jay@jay.fm, Blair Zajac <blair@orcaware.com>* - -* Improve error message when nil is assigned to an attr which validates_size_of within a range. #2022 *Manuel Holtgrewe <purestorm@ggnore.net>* - -* Make update_attribute use the same writer method that update_attributes uses. - \#2237 [trevor@protocool.com] -* Make migrations honor table name prefixes and suffixes. #2298 *Jakob Skjerning, Marcel Molina Jr.* - -* Correct and optimize PostgreSQL bytea escaping. #1745, #1837 *dave@cherryville.org, ken@miriamtech.com, bellis@deepthought.org* - -* Fixtures should only reset a PostgreSQL sequence if it corresponds to an integer primary key named id. #1749 *chris@chrisbrinker.com* - -* Standardize the interpretation of boolean columns in the Mysql and Sqlite adapters. (Use MysqlAdapter.emulate_booleans = false to disable this behavior) - -* Added new symbol-driven approach to activating observers with Base#observers= [David Heinemeier Hansson]. Example: - - ActiveRecord::Base.observers = :cacher, :garbage_collector - -* Added AbstractAdapter#select_value and AbstractAdapter#select_values as convenience methods for selecting single values, instead of hashes, of the first column in a SELECT #2283 *solo@gatelys.com* - -* Wrap :conditions in parentheses to prevent problems with OR's #1871 *Jamis Buck* - -* Allow the postgresql adapter to work with the SchemaDumper. *Jamis Buck* - -* Add ActiveRecord::SchemaDumper for dumping a DB schema to a pure-ruby file, making it easier to consolidate large migration lists and port database schemas between databases. *Jamis Buck* - -* Fixed migrations for Windows when using more than 10 *David Naseby* - -* Fixed that the create_x method from belongs_to wouldn't save the association properly #2042 *Florian Weber* - -* Fixed saving a record with two unsaved belongs_to associations pointing to the same object #2023 *Tobias Lütke* - -* Improved migrations' behavior when the schema_info table is empty. *Nicholas Seckar* - -* Fixed that Observers didn't observe sub-classes #627 *Florian Weber* - -* Fix eager loading error messages, allow :include to specify tables using strings or symbols. Closes #2222 *Marcel Molina Jr.* - -* Added check for RAILS_CONNECTION_ADAPTERS on startup and only load the connection adapters specified within if its present (available in Rails through config.connection_adapters using the new config) #1958 *skae* - -* Fixed various problems with has_and_belongs_to_many when using customer finder_sql #2094 *Florian Weber* - -* Added better exception error when unknown column types are used with migrations #1814 *François Beausoleil* - -* Fixed "connection lost" issue with the bundled Ruby/MySQL driver (would kill the app after 8 hours of inactivity) #2163, #428 *kajism@yahoo.com* - -* Fixed comparison of Active Record objects so two new objects are not equal #2099 *deberg* - -* Fixed that the SQL Server adapter would sometimes return DBI::Timestamp objects instead of Time #2127 *Tom Ward* - -* Added the instance methods #root and #ancestors on acts_as_tree and fixed siblings to not include the current node #2142, #2140 *coffee2code* - -* Fixed that Active Record would call SHOW FIELDS twice (or more) for the same model when the cached results were available #1947 *sd@notso.net* - -* Added log_level and use_silence parameter to ActiveRecord::Base.benchmark. The first controls at what level the benchmark statement will be logged (now as debug, instead of info) and the second that can be passed false to include all logging statements during the benchmark block/ - -* Make sure the schema_info table is created before querying the current version #1903 - -* Fixtures ignore table name prefix and suffix #1987 *Jakob Skjerning* - -* Add documentation for index_type argument to add_index method for migrations #2005 *Blaine* - -* Modify read_attribute to allow a symbol argument #2024 *Ken Kunz* - -* Make destroy return self #1913 *Sebastian Kanthak* - -* Fix typo in validations documentation #1938 *court3nay* - -* Make acts_as_list work for insert_at(1) #1966 *hensleyl@papermountain.org* - -* Fix typo in count_by_sql documentation #1969 *Alexey Verkhovsky* - -* Allow add_column and create_table to specify NOT NULL #1712 *emptysands@gmail.com* - -* Fix create_table so that id column is implicitly added *Rick Olson* - -* Default sequence names for Oracle changed to #{table_name}_seq, which is the most commonly used standard. In addition, a new method ActiveRecord::Base#set_sequence_name allows the developer to set the sequence name per model. This is a non-backwards-compatible change -- anyone using the old-style "rails_sequence" will need to either create new sequences, or set: ActiveRecord::Base.set_sequence_name = "rails_sequence" #1798 - -* OCIAdapter now properly handles synonyms, which are commonly used to separate out the schema owner from the application user #1798 - -* Fixed the handling of camelCase columns names in Oracle #1798 - -* Implemented for OCI the Rakefile tasks of :clone_structure_to_test, :db_structure_dump, and :purge_test_database, which enable Oracle folks to enjoy all the agile goodness of Rails for testing. Note that the current implementation is fairly limited -- only tables and sequences are cloned, not constraints or indexes. A full clone in Oracle generally requires some manual effort, and is version-specific. Post 9i, Oracle recommends the use of the DBMS_METADATA package, though that approach requires editing of the physical characteristics generated #1798 - -* Fixed the handling of multiple blob columns in Oracle if one or more of them are null #1798 - -* Added support for calling constrained class methods on has_many and has_and_belongs_to_many collections #1764 *Tobias Lütke* - - class Comment < AR:B - def self.search(q) - find(:all, :conditions => ["body = ?", q]) - end - end - - class Post < AR:B - has_many :comments - end - - Post.find(1).comments.search('hi') # => SELECT * from comments WHERE post_id = 1 AND body = 'hi' - - NOTICE: This patch changes the underlying SQL generated by has_and_belongs_to_many queries. If your relying on that, such as - by explicitly referencing the old t and j aliases, you'll need to update your code. Of course, you _shouldn't_ be relying on - details like that no less than you should be diving in to touch private variables. But just in case you do, consider yourself - noticed :) - -* Added migration support for SQLite (using temporary tables to simulate ALTER TABLE) #1771 *Sam Stephenson* - -* Remove extra definition of supports_migrations? from abstract_adaptor.rb *Nicholas Seckar* - -* Fix acts_as_list so that moving next-to-last item to the bottom does not result in duplicate item positions - -* Fixed incompatibility in DB2 adapter with the new limit/offset approach #1718 *Maik Schmidt* - -* Added :select option to find which can specify a different value than the default *, like find(:all, :select => "first_name, last_name"), if you either only want to select part of the columns or exclude columns otherwise included from a join #1338 *Stefan Kaes* - - -## 1.11.1 (11 July, 2005) ## - -* Added support for limit and offset with eager loading of has_one and belongs_to associations. Using the options with has_many and has_and_belongs_to_many associations will now raise an ActiveRecord::ConfigurationError #1692 *Rick Olson* - -* Fixed that assume_bottom_position (in acts_as_list) could be called on items already last in the list and they would move one position away from the list #1648 *tyler@kianta.com* - -* Added ActiveRecord::Base.threaded_connections flag to turn off 1-connection per thread (required for thread safety). By default it's on, but WEBrick in Rails need it off #1685 *Sam Stephenson* - -* Correct reflected table name for singular associations. #1688 *court3nay* - -* Fixed optimistic locking with SQL Server #1660 *tom@popdog.net* - -* Added ActiveRecord::Migrator.migrate that can figure out whether to go up or down based on the target version and the current - -* Added better error message for "packets out of order" #1630 *court3nay* - -* Fixed first run of "rake migrate" on PostgreSQL by not expecting a return value on the id #1640 - - -## 1.11.0 (6 July, 2005) ## - -* Fixed that Yaml error message in fixtures hid the real error #1623 *Nicholas Seckar* - -* Changed logging of SQL statements to use the DEBUG level instead of INFO - -* Added new Migrations framework for describing schema transformations in a way that can be easily applied across multiple databases #1604 [Tobias Lütke] See documentation under ActiveRecord::Migration and the additional support in the Rails rakefile/generator. - -* Added callback hooks to association collections #1549 [Florian Weber]. Example: - - class Project - has_and_belongs_to_many :developers, :before_add => :evaluate_velocity - - def evaluate_velocity(developer) - ... - end - end - - ..raising an exception will cause the object not to be added (or removed, with before_remove). - - -* Fixed Base.content_columns call for SQL Server adapter #1450 *DeLynn Berry* - -* Fixed Base#write_attribute to work with both symbols and strings #1190 *Paul Legato* - -* Fixed that has_and_belongs_to_many didn't respect single table inheritance types #1081 *Florian Weber* - -* Speed up ActiveRecord#method_missing for the common case (read_attribute). - -* Only notify observers on after_find and after_initialize if these methods are defined on the model. #1235 *Stefan Kaes* - -* Fixed that single-table inheritance sub-classes couldn't be used to limit the result set with eager loading #1215 *Chris McGrath* - -* Fixed validates_numericality_of to work with overrided getter-method when :allow_nil is on #1316 *raidel@onemail.at* - -* Added roots, root, and siblings to the batch of methods added by acts_as_tree #1541 *Michael Schuerig* - -* Added support for limit/offset with the MS SQL Server driver so that pagination will now work #1569 *DeLynn Berry* - -* Added support for ODBC connections to MS SQL Server so you can connect from a non-Windows machine #1569 *Mark Imbriaco/DeLynn Berry* - -* Fixed that multiparameter posts ignored attr_protected #1532 *alec+rails@veryclever.net* - -* Fixed problem with eager loading when using a has_and_belongs_to_many association using :association_foreign_key #1504 *flash@vanklinkenbergsoftware.nl* - -* Fixed Base#find to honor the documentation on how :joins work and make them consistent with Base#count #1405 [pritchie@gmail.com]. What used to be: - - Developer.find :all, :joins => 'developers_projects', :conditions => 'id=developer_id AND project_id=1' - - ...should instead be: - - Developer.find( - :all, - :joins => 'LEFT JOIN developers_projects ON developers.id = developers_projects.developer_id', - :conditions => 'project_id=1' - ) - -* Fixed that validations didn't respecting custom setting for too_short, too_long messages #1437 *Marcel Molina Jr.* - -* Fixed that clear_association_cache doesn't delete new associations on new records (so you can safely place new records in the session with Action Pack without having new associations wiped) #1494 *cluon* - -* Fixed that calling Model.find([]) returns [] and doesn't throw an exception #1379 - -* Fixed that adding a record to a has_and_belongs_to collection would always save it -- now it only saves if its a new record #1203 *Alisdair McDiarmid* - -* Fixed saving of in-memory association structures to happen as an after_create/after_update callback instead of after_save -- that way you can add new associations in after_create/after_update callbacks without getting them saved twice - -* Allow any Enumerable, not just Array, to work as bind variables #1344 *Jeremy Kemper* - -* Added actual database-changing behavior to collection assigment for has_many and has_and_belongs_to_many #1425 [Sebastian Kanthak]. - Example: - - david.projects = [Project.find(1), Project.new("name" => "ActionWebSearch")] - david.save - - If david.projects already contain the project with ID 1, this is left unchanged. Any other projects are dropped. And the new - project is saved when david.save is called. - - Also included is a way to do assignments through IDs, which is perfect for checkbox updating, so you get to do: - - david.project_ids = [1, 5, 7] - -* Corrected typo in find SQL for has_and_belongs_to_many. #1312 *ben@bensinclair.com* - -* Fixed sanitized conditions for has_many finder method. #1281 *jackc@hylesanderson.com, pragdave, Tobias Lütke* - -* Comprehensive PostgreSQL schema support. Use the optional schema_search_path directive in database.yml to give a comma-separated list of schemas to search for your tables. This allows you, for example, to have tables in a shared schema without having to use a custom table name. See http://www.postgresql.org/docs/8.0/interactive/ddl-schemas.html to learn more. #827 *dave@cherryville.org* - -* Corrected @@configurations typo #1410 *david@ruppconsulting.com* - -* Return PostgreSQL columns in the order they were declared #1374 *perlguy@gmail.com* - -* Allow before/after update hooks to work on models using optimistic locking - -* Eager loading of dependent has_one associations won't delete the association #1212 - -* Added a second parameter to the build and create method for has_one that controls whether the existing association should be replaced (which means nullifying its foreign key as well). By default this is true, but false can be passed to prevent it. - -* Using transactional fixtures now causes the data to be loaded only once. - -* Added fixture accessor methods that can be used when instantiated fixtures are disabled. - - fixtures :web_sites - - def test_something - assert_equal "Ruby on Rails", web_sites(:rubyonrails).name - end - -* Added DoubleRenderError exception that'll be raised if render* is called twice #518 *Nicholas Seckar* - -* Fixed exceptions occuring after render has been called #1096 *Nicholas Seckar* - -* CHANGED: validates_presence_of now uses Errors#add_on_blank, which will make " " fail the validation where it didn't before #1309 - -* Added Errors#add_on_blank which works like Errors#add_on_empty, but uses Object#blank? instead - -* Added the :if option to all validations that can either use a block or a method pointer to determine whether the validation should be run or not. #1324 [Duane Johnson/jhosteny]. Examples: - - Conditional validations such as the following are made possible: - validates_numericality_of :income, :if => :employed? - - Conditional validations can also solve the salted login generator problem: - validates_confirmation_of :password, :if => :new_password? - - Using blocks: - validates_presence_of :username, :if => Proc.new { |user| user.signup_step > 1 } - -* Fixed use of construct_finder_sql when using :join #1288 *dwlt@dwlt.net* - -* Fixed that :delete_sql in has_and_belongs_to_many associations couldn't access record properties #1299 *Rick Olson* - -* Fixed that clone would break when an aggregate had the same name as one of its attributes #1307 *Jeremy Kemper* - -* Changed that destroying an object will only freeze the attributes hash, which keeps the object from having attributes changed (as that wouldn't make sense), but allows for the querying of associations after it has been destroyed. - -* Changed the callbacks such that observers are notified before the in-object callbacks are triggered. Without this change, it wasn't possible to act on the whole object in something like a before_destroy observer without having the objects own callbacks (like deleting associations) called first. - -* Added option for passing an array to the find_all version of the dynamic finders and have it evaluated as an IN fragment. Example: - - # SELECT * FROM topics WHERE title IN ('First', 'Second') - Topic.find_all_by_title(["First", "Second"]) - -* Added compatibility with camelCase column names for dynamic finders #533 *Dee Zsombor* - -* Fixed extraneous comma in count() function that made it not work with joins #1156 *Jarkko Laine/Dee Zsombor* - -* Fixed incompatibility with Base#find with an array of ids that would fail when using eager loading #1186 *Alisdair McDiarmid* - -* Fixed that validate_length_of lost :on option when :within was specified #1195 *jhosteny@mac.com* - -* Added encoding and min_messages options for PostgreSQL #1205 [Shugo Maeda]. Configuration example: - - development: - adapter: postgresql - database: rails_development - host: localhost - username: postgres - password: - encoding: UTF8 - min_messages: ERROR - -* Fixed acts_as_list where deleting an item that was removed from the list would ruin the positioning of other list items #1197 *Jamis Buck* - -* Added validates_exclusion_of as a negative of validates_inclusion_of - -* Optimized counting of has_many associations by setting the association to empty if the count is 0 so repeated calls doesn't trigger database calls - - -## 1.10.1 (20th April, 2005) ## - -* Fixed frivilous database queries being triggered with eager loading on empty associations and other things - -* Fixed order of loading in eager associations - -* Fixed stray comma when using eager loading and ordering together from has_many associations #1143 - - -## 1.10.0 (19th April, 2005) ## - -* Added eager loading of associations as a way to solve the N+1 problem more gracefully without piggy-back queries. Example: - - for post in Post.find(:all, :limit => 100) - puts "Post: " + post.title - puts "Written by: " + post.author.name - puts "Last comment on: " + post.comments.first.created_on - end - - This used to generate 301 database queries if all 100 posts had both author and comments. It can now be written as: - - for post in Post.find(:all, :limit => 100, :include => [ :author, :comments ]) - - ...and the number of database queries needed is now 1. - -* Added new unified Base.find API and deprecated the use of find_first and find_all. See the documentation for Base.find. Examples: - - Person.find(1, :conditions => "administrator = 1", :order => "created_on DESC") - Person.find(1, 5, 6, :conditions => "administrator = 1", :order => "created_on DESC") - Person.find(:first, :order => "created_on DESC", :offset => 5) - Person.find(:all, :conditions => [ "category IN (?)", categories], :limit => 50) - Person.find(:all, :offset => 10, :limit => 10) - -* Added acts_as_nested_set #1000 [wschenk]. Introduction: - - This acts provides Nested Set functionality. Nested Set is similiar to Tree, but with - the added feature that you can select the children and all of it's descendants with - a single query. A good use case for this is a threaded post system, where you want - to display every reply to a comment without multiple selects. - -* Added Base.save! that attempts to save the record just like Base.save but will raise a RecordInvalid exception instead of returning false if the record is not valid *Dave Thomas* - -* Fixed PostgreSQL usage of fixtures with regards to public schemas and table names with dots #962 *gnuman1@gmail.com* - -* Fixed that fixtures were being deleted in the same order as inserts causing FK errors #890 *andrew.john.peters@gmail.com* - -* Fixed loading of fixtures in to be in the right order (or PostgreSQL would bark) #1047 *stephenh@chase3000.com* - -* Fixed page caching for non-vhost applications living underneath the root #1004 *Ben Schumacher* - -* Fixes a problem with the SQL Adapter which was resulting in IDENTITY_INSERT not being set to ON when it should be #1104 *adelle* - -* Added the option to specify the acceptance string in validates_acceptance_of #1106 *caleb@aei-tech.com* - -* Added insert_at(position) to acts_as_list #1083 *DeLynnB* - -* Removed the default order by id on has_and_belongs_to_many queries as it could kill performance on large sets (you can still specify by hand with :order) - -* Fixed that Base.silence should restore the old logger level when done, not just set it to DEBUG #1084 *yon@milliped.com* - -* Fixed boolean saving on Oracle #1093 *mparrish@pearware.org* - -* Moved build_association and create_association for has_one and belongs_to out of deprecation as they work when the association is nil unlike association.build and association.create, which require the association to be already in place #864 - -* Added rollbacks of transactions if they're active as the dispatcher is killed gracefully (TERM signal) #1054 *Leon Bredt* - -* Added quoting of column names for fixtures #997 *jcfischer@gmail.com* - -* Fixed counter_sql when no records exist in database for PostgreSQL (would give error, not 0) #1039 *Caleb Tennis* - -* Fixed that benchmarking times for rendering included db runtimes #987 *Stefan Kaes* - -* Fixed boolean queries for t/f fields in PostgreSQL #995 *dave@cherryville.org* - -* Added that model.items.delete(child) will delete the child, not just set the foreign key to nil, if the child is dependent on the model #978 *Jeremy Kemper* - -* Fixed auto-stamping of dates (created_on/updated_on) for PostgreSQL #985 *dave@cherryville.org* - -* Fixed Base.silence/benchmark to only log if a logger has been configured #986 *Stefan Kaes* - -* Added a join parameter as the third argument to Base.find_first and as the second to Base.count #426, #988 *Stefan Kaes* - -* Fixed bug in Base#hash method that would treat records with the same string-based id as different *Dave Thomas* - -* Renamed DateHelper#distance_of_time_in_words_to_now to DateHelper#time_ago_in_words (old method name is still available as a deprecated alias) - - -## 1.9.1 (27th March, 2005) ## - -* Fixed that Active Record objects with float attribute could not be cloned #808 - -* Fixed that MissingSourceFile's wasn't properly detected in production mode #925 *Nicholas Seckar* - -* Fixed that :counter_cache option would look for a line_items_count column for a LineItem object instead of lineitems_count - -* Fixed that AR exists?() would explode on postgresql if the passed id did not match the PK type #900 *Scott Barron* - -* Fixed the MS SQL adapter to work with the new limit/offset approach and with binary data (still suffering from 7KB limit, though) #901 *delynnb* - - -## 1.9.0 (22th March, 2005) ## - -* Added adapter independent limit clause as a two-element array with the first being the limit, the second being the offset #795 [Sam Stephenson]. Example: - - Developer.find_all nil, 'id ASC', 5 # return the first five developers - Developer.find_all nil, 'id ASC', [3, 8] # return three developers, starting from #8 and forward - - This doesn't yet work with the DB2 or MS SQL adapters. Patches to make that happen are encouraged. - -* Added alias_method :to_param, :id to Base, such that Active Record objects to be used as URL parameters in Action Pack automatically #812 *Nicholas Seckar/Sam Stephenson* - -* Improved the performance of the OCI8 adapter for Oracle #723 *pilx/gjenkins* - -* Added type conversion before saving a record, so string-based values like "10.0" aren't left for the database to convert #820 *dave@cherryville.org* - -* Added with additional settings for working with transactional fixtures and pre-loaded test databases #865 *mindel* - -* Fixed acts_as_list to trigger remove_from_list on destroy after the fact, not before, so a unique position can be maintained #871 *Alisdair McDiarmid* - -* Added the possibility of specifying fixtures in multiple calls #816 *kim@tinker.com* - -* Added Base.exists?(id) that'll return true if an object of the class with the given id exists #854 *stian@grytoyr.net* - -* Added optionally allow for nil or empty strings with validates_numericality_of #801 *Sebastian Kanthak* - -* Fixed problem with using slashes in validates_format_of regular expressions #801 *Sebastian Kanthak* - -* Fixed that SQLite3 exceptions are caught and reported properly #823 *yerejm* - -* Added that all types of after_find/after_initialized callbacks are triggered if the explicit implementation is present, not only the explicit implementation itself - -* Fixed that symbols can be used on attribute assignment, like page.emails.create(:subject => data.subject, :body => data.body) - - -## 1.8.0 (7th March, 2005) ## - -* Added ActiveRecord::Base.colorize_logging to control whether to use colors in logs or not (on by default) - -* Added support for timestamp with time zone in PostgreSQL #560 *Scott Barron* - -* Added MultiparameterAssignmentErrors and AttributeAssignmentError exceptions #777 [demetrius]. Documentation: - - * +MultiparameterAssignmentErrors+ -- collection of errors that occurred during a mass assignment using the - +attributes=+ method. The +errors+ property of this exception contains an array of +AttributeAssignmentError+ - objects that should be inspected to determine which attributes triggered the errors. - * +AttributeAssignmentError+ -- an error occurred while doing a mass assignment through the +attributes=+ method. - You can inspect the +attribute+ property of the exception object to determine which attribute triggered the error. - -* Fixed that postgresql adapter would fails when reading bytea fields with null value #771 *rodrigo k* - -* Added transactional fixtures that uses rollback to undo changes to fixtures instead of DELETE/INSERT -- it's much faster. See documentation under Fixtures #760 *Jeremy Kemper* - -* Added destruction of dependent objects in has_one associations when a new assignment happens #742 [mindel]. Example: - - class Account < ActiveRecord::Base - has_one :credit_card, :dependent => true - end - class CreditCard < ActiveRecord::Base - belongs_to :account - end - - account.credit_card # => returns existing credit card, lets say id = 12 - account.credit_card = CreditCard.create("number" => "123") - account.save # => CC with id = 12 is destroyed - - -* Added validates_numericality_of #716 [Sebastian Kanthak/Chris McGrath]. Docuemntation: - - Validates whether the value of the specified attribute is numeric by trying to convert it to - a float with Kernel.Float (if <tt>integer</tt> is false) or applying it to the regular expression - <tt>/^[\+\-]?\d+$/</tt> (if <tt>integer</tt> is set to true). - - class Person < ActiveRecord::Base - validates_numericality_of :value, :on => :create - end - - Configuration options: - * <tt>message</tt> - A custom error message (default is: "is not a number") - * <tt>on</tt> Specifies when this validation is active (default is :save, other options :create, :update) - * <tt>only_integer</tt> Specifies whether the value has to be an integer, e.g. an integral value (default is false) - - -* Fixed that HasManyAssociation#count was using :finder_sql rather than :counter_sql if it was available #445 *Scott Barron* - -* Added better defaults for composed_of, so statements like composed_of :time_zone, :mapping => %w( time_zone time_zone ) can be written without the mapping part (it's now assumed) - -* Added MacroReflection#macro which will return a symbol describing the macro used (like :composed_of or :has_many) #718, #248 *james@slashetc.com* - - -## 1.7.0 (24th February, 2005) ## - -* Changed the auto-timestamping feature to use ActiveRecord::Base.default_timezone instead of entertaining the parallel ActiveRecord::Base.timestamps_gmt method. The latter is now deprecated and will throw a warning on use (but still work) #710 *Jamis Buck* - -* Added a OCI8-based Oracle adapter that has been verified to work with Oracle 8 and 9 #629 [Graham Jenkins]. Usage notes: - - 1. Key generation uses a sequence "rails_sequence" for all tables. (I couldn't find a simple - and safe way of passing table-specific sequence information to the adapter.) - 2. Oracle uses DATE or TIMESTAMP datatypes for both dates and times. Consequently I have had to - resort to some hacks to get data converted to Date or Time in Ruby. - If the column_name ends in _at (like created_at, updated_at) it's created as a Ruby Time. Else if the - hours/minutes/seconds are 0, I make it a Ruby Date. Else it's a Ruby Time. - This is nasty - but if you use Duck Typing you'll probably not care very much. - In 9i it's tempting to map DATE to Date and TIMESTAMP to Time but I don't think that is - valid - too many databases use DATE for both. - Timezones and sub-second precision on timestamps are not supported. - 3. Default values that are functions (such as "SYSDATE") are not supported. This is a - restriction of the way active record supports default values. - 4. Referential integrity constraints are not fully supported. Under at least - some circumstances, active record appears to delete parent and child records out of - sequence and out of transaction scope. (Or this may just be a problem of test setup.) - - The OCI8 driver can be retrieved from http://rubyforge.org/projects/ruby-oci8/ - -* Added option :schema_order to the PostgreSQL adapter to support the use of multiple schemas per database #697 *YuriSchimke* - -* Optimized the SQL used to generate has_and_belongs_to_many queries by listing the join table first #693 *yerejm* - -* Fixed that when using validation macros with a custom message, if you happened to use single quotes in the message string you would get a parsing error #657 *tonka* - -* Fixed that Active Record would throw Broken Pipe errors with FCGI when the MySQL connection timed out instead of reconnecting #428 *Nicholas Seckar* - -* Added options to specify an SSL connection for MySQL. Define the following attributes in the connection config (config/database.yml in Rails) to use it: sslkey, sslcert, sslca, sslcapath, sslcipher. To use SSL with no client certs, just set :sslca = '/dev/null'. http://dev.mysql.com/doc/mysql/en/secure-connections.html #604 *daniel@nightrunner.com* - -* Added automatic dropping/creating of test tables for running the unit tests on all databases #587 *adelle@bullet.net.au* - -* Fixed that find_by_* would fail when column names had numbers #670 *demetrius* - -* Fixed the SQL Server adapter on a bunch of issues #667 *DeLynn* - - 1. Created a new columns method that is much cleaner. - 2. Corrected a problem with the select and select_all methods - that didn't account for the LIMIT clause being passed into raw SQL statements. - 3. Implemented the string_to_time method in order to create proper instances of the time class. - 4. Added logic to the simplified_type method that allows the database to specify the scale of float data. - 5. Adjusted the quote_column_name to account for the fact that MS SQL is bothered by a forward slash in the data string. - -* Fixed that the dynamic finder like find_all_by_something_boolean(false) didn't work #649 *lmarlow* - -* Added validates_each that validates each specified attribute against a block #610 [Jeremy Kemper]. Example: - - class Person < ActiveRecord::Base - validates_each :first_name, :last_name do |record, attr| - record.errors.add attr, 'starts with z.' if attr[0] == ?z - end - end - -* Added :allow_nil as an explicit option for validates_length_of, so unless that's set to true having the attribute as nil will also return an error if a range is specified as :within #610 *Jeremy Kemper* - -* Added that validates_* now accept blocks to perform validations #618 [Tim Bates]. Example: - - class Person < ActiveRecord::Base - validate { |person| person.errors.add("title", "will never be valid") if SHOULD_NEVER_BE_VALID } - end - -* Addded validation for validate all the associated objects before declaring failure with validates_associated #618 *Tim Bates* - -* Added keyword-style approach to defining the custom relational bindings #545 [Jamis Buck]. Example: - - class Project < ActiveRecord::Base - primary_key "sysid" - table_name "XYZ_PROJECT" - inheritance_column { original_inheritance_column + "_id" } - end - -* Fixed Base#clone for use with PostgreSQL #565 *hanson@surgery.wisc.edu* - - -## 1.6.0 (January 25th, 2005) ## - -* Added that has_many association build and create methods can take arrays of record data like Base#create and Base#build to build/create multiple records at once. - -* Added that Base#delete and Base#destroy both can take an array of ids to delete/destroy #336 - -* Added the option of supplying an array of attributes to Base#create, so that multiple records can be created at once. - -* Added the option of supplying an array of ids and attributes to Base#update, so that multiple records can be updated at once (inspired by #526/Duane Johnson). Example - - people = { 1 => { "first_name" => "David" }, 2 => { "first_name" => "Jeremy"} } - Person.update(people.keys, people.values) - -* Added ActiveRecord::Base.timestamps_gmt that can be set to true to make the automated timestamping use GMT instead of local time #520 *Scott Baron* - -* Added that update_all calls sanitize_sql on its updates argument, so stuff like MyRecord.update_all(['time = ?', Time.now]) works #519 *notahat* - -* Fixed that the dynamic finders didn't treat nil as a "IS NULL" but rather "= NULL" case #515 *Demetrius* - -* Added bind-named arrays for interpolating a group of ids or strings in conditions #528 *Jeremy Kemper* - -* Added that has_and_belongs_to_many associations with additional attributes also can be created between unsaved objects and only committed to the database when Base#save is called on the associator #524 *Eric Anderson* - -* Fixed that records fetched with piggy-back attributes or through rich has_and_belongs_to_many associations couldn't be saved due to the extra attributes not part of the table #522 *Eric Anderson* - -* Added mass-assignment protection for the inheritance column -- regardless of a custom column is used or not - -* Fixed that association proxies would fail === tests like PremiumSubscription === @account.subscription - -* Fixed that column aliases didn't work as expected with the new MySql411 driver #507 *Demetrius* - -* Fixed that find_all would produce invalid sql when called sequentialy #490 *Scott Baron* - - -## 1.5.1 (January 18th, 2005) ## - -* Fixed that the belongs_to and has_one proxy would fail a test like 'if project.manager' -- this unfortunately also means that you can't call methods like project.manager.build unless there already is a manager on the project #492 *Tim Bates* - -* Fixed that the Ruby/MySQL adapter wouldn't connect if the password was empty #503 *Pelle* - - -## 1.5.0 (January 17th, 2005) ## - -* Fixed that unit tests for MySQL are now run as the "rails" user instead of root #455 *Eric Hodel* - -* Added validates_associated that enables validation of objects in an unsaved association #398 [Tim Bates]. Example: - - class Book < ActiveRecord::Base - has_many :pages - belongs_to :library - - validates_associated :pages, :library - end - -* Added support for associating unsaved objects #402 [Tim Bates]. Rules that govern this addition: - - == Unsaved objects and associations - - You can manipulate objects and associations before they are saved to the database, but there is some special behaviour you should be - aware of, mostly involving the saving of associated objects. - - === One-to-one associations - - * Assigning an object to a has_one association automatically saves that object, and the object being replaced (if there is one), in - order to update their primary keys - except if the parent object is unsaved (new_record? == true). - * If either of these saves fail (due to one of the objects being invalid) the assignment statement returns false and the assignment - is cancelled. - * If you wish to assign an object to a has_one association without saving it, use the #association.build method (documented below). - * Assigning an object to a belongs_to association does not save the object, since the foreign key field belongs on the parent. It does - not save the parent either. - - === Collections - - * Adding an object to a collection (has_many or has_and_belongs_to_many) automatically saves that object, except if the parent object - (the owner of the collection) is not yet stored in the database. - * If saving any of the objects being added to a collection (via #push or similar) fails, then #push returns false. - * You can add an object to a collection without automatically saving it by using the #collection.build method (documented below). - * All unsaved (new_record? == true) members of the collection are automatically saved when the parent is saved. - -* Added replace to associations, so you can do project.manager.replace(new_manager) or project.milestones.replace(new_milestones) #402 *Tim Bates* - -* Added build and create methods to has_one and belongs_to associations, so you can now do project.manager.build(attributes) #402 *Tim Bates* - -* Added that if a before_* callback returns false, all the later callbacks and the associated action are cancelled. If an after_* callback returns false, all the later callbacks are cancelled. Callbacks are generally run in the order they are defined, with the exception of callbacks defined as methods on the model, which are called last. #402 *Tim Bates* - -* Fixed that Base#== wouldn't work for multiple references to the same unsaved object #402 *Tim Bates* - -* Fixed binary support for PostgreSQL #444 *alex@byzantine.no* - -* Added a differenciation between AssociationCollection#size and -length. Now AssociationCollection#size returns the size of the - collection by executing a SELECT COUNT(*) query if the collection hasn't been loaded and calling collection.size if it has. If - it's more likely than not that the collection does have a size larger than zero and you need to fetch that collection afterwards, - it'll take one less SELECT query if you use length. - -* Added Base#attributes that returns a hash of all the attributes with their names as keys and clones of their objects as values #433 *atyp.de* - -* Fixed that foreign keys named the same as the association would cause stack overflow #437 *Eric Anderson* - -* Fixed default scope of acts_as_list from "1" to "1 = 1", so it'll work in PostgreSQL (among other places) #427 *Alexey* - -* Added Base#reload that reloads the attributes of an object from the database #422 *Andreas Schwarz* - -* Added SQLite3 compatibility through the sqlite3-ruby adapter by Jamis Buck #381 *Jeremy Kemper* - -* Added support for the new protocol spoken by MySQL 4.1.1+ servers for the Ruby/MySQL adapter that ships with Rails #440 *Matt Mower* - -* Added that Observers can use the observes class method instead of overwriting self.observed_class(). - - Before: - class ListSweeper < ActiveRecord::Base - def self.observed_class() [ List, Item ] - end - - After: - class ListSweeper < ActiveRecord::Base - observes List, Item - end - -* Fixed that conditions in has_many and has_and_belongs_to_many should be interpolated just like the finder_sql is - -* Fixed Base#update_attribute to be indifferent to whether a string or symbol is used to describe the name - -* Added Base#toggle(attribute) and Base#toggle!(attribute) that makes it easier to flip a switch or flag. - - Before: topic.update_attribute(:approved, !approved?) - After : topic.toggle!(:approved) - -* Added Base#increment!(attribute) and Base#decrement!(attribute) that also saves the records. Example: - - page.views # => 1 - page.increment!(:views) # executes an UPDATE statement - page.views # => 2 - - page.increment(:views).increment!(:views) - page.views # => 4 - -* Added Base#increment(attribute) and Base#decrement(attribute) that encapsulates the += 1 and -= 1 patterns. - - -## 1.4.0 (January 4th, 2005) ## - -* Added automated optimistic locking if the field <tt>lock_version</tt> is present. Each update to the - record increments the lock_version column and the locking facilities ensure that records instantiated twice - will let the last one saved raise a StaleObjectError if the first was also updated. Example: - - p1 = Person.find(1) - p2 = Person.find(1) - - p1.first_name = "Michael" - p1.save - - p2.first_name = "should fail" - p2.save # Raises a ActiveRecord::StaleObjectError - - You're then responsible for dealing with the conflict by rescuing the exception and either rolling back, merging, - or otherwise apply the business logic needed to resolve the conflict. - - \#384 *Michael Koziarski* - -* Added dynamic attribute-based finders as a cleaner way of getting objects by simple queries without turning to SQL. - They work by appending the name of an attribute to <tt>find_by_</tt>, so you get finders like <tt>Person.find_by_user_name, - Payment.find_by_transaction_id</tt>. So instead of writing <tt>Person.find_first(["user_name = ?", user_name])</tt>, you just do - <tt>Person.find_by_user_name(user_name)</tt>. - - It's also possible to use multiple attributes in the same find by separating them with "_and_", so you get finders like - <tt>Person.find_by_user_name_and_password</tt> or even <tt>Payment.find_by_purchaser_and_state_and_country</tt>. So instead of writing - <tt>Person.find_first(["user_name = ? AND password = ?", user_name, password])</tt>, you just do - <tt>Person.find_by_user_name_and_password(user_name, password)</tt>. - - While primarily a construct for easier find_firsts, it can also be used as a construct for find_all by using calls like - <tt>Payment.find_all_by_amount(50)</tt> that is turned into <tt>Payment.find_all(["amount = ?", 50])</tt>. This is something not as equally useful, - though, as it's not possible to specify the order in which the objects are returned. - -* Added block-style for callbacks #332 [Jeremy Kemper]. - - Before: - before_destroy(Proc.new{ |record| Person.destroy_all "firm_id = #{record.id}" }) - - After: - before_destroy { |record| Person.destroy_all "firm_id = #{record.id}" } - -* Added :counter_cache option to acts_as_tree that works just like the one you can define on belongs_to #371 *Josh Peek* - -* Added Base.default_timezone accessor that determines whether to use Time.local (using :local) or Time.utc (using :utc) when pulling dates - and times from the database. This is set to :local by default. - -* Added the possibility for adapters to overwrite add_limit! to implement a different limiting scheme than "LIMIT X" used by MySQL, PostgreSQL, and SQLite. - -* Added the possibility of having objects with acts_as_list created before their scope is available or... - -* Added a db2 adapter that only depends on the Ruby/DB2 bindings (http://raa.ruby-lang.org/project/ruby-db2/) #386 *Maik Schmidt* - -* Added the final touches to the Microsoft SQL Server adapter by Joey Gibson that makes it suitable for actual use #394 *DeLynn Barry* - -* Added that Base#find takes an optional options hash, including :conditions. Base#find_on_conditions deprecated in favor of #find with :conditions #407 *Jeremy Kemper* - -* Added HasManyAssociation#count that works like Base#count #413 *intinig* - -* Fixed handling of binary content in blobs and similar fields for Ruby/MySQL and SQLite #409 *xal* - -* Fixed a bug in the Ruby/MySQL that caused binary content to be escaped badly and come back mangled #405 *Tobias Lütke* - -* Fixed that the const_missing autoload assumes the requested constant is set by require_association and calls const_get to retrieve it. - If require_association did not set the constant then const_get will call const_missing, resulting in an infinite loop #380 *Jeremy Kemper* - -* Fixed broken transactions that were actually only running object-level and not db level transactions *andreas* - -* Fixed that validates_uniqueness_of used 'id' instead of defined primary key #406 - -* Fixed that the overwritten respond_to? method didn't take two parameters like the original #391 - -* Fixed quoting in validates_format_of that would allow some rules to pass regardless of input #390 *Dmitry V. Sabanin* - - -## 1.3.0 (December 23, 2004) ## - -* Added a require_association hook on const_missing that makes it possible to use any model class without requiring it first. This makes STI look like: - - before: - require_association 'person' - class Employee < Person - end - - after: - class Employee < Person - end - - This also reduces the usefulness of Controller.model in Action Pack to currently only being for documentation purposes. - -* Added that Base.update_all and Base.delete_all return an integer of the number of affected rows #341 - -* Added scope option to validation_uniqueness #349 *Kent Sibilev* - -* Added respondence to *_before_type_cast for all attributes to return their string-state before they were type casted by the column type. - This is helpful for getting "100,000" back on a integer-based validation where the value would normally be "100". - -* Added allow_nil options to validates_inclusion_of so that validation is only triggered if the attribute is not nil *what-a-day* - -* Added work-around for PostgreSQL and the problem of getting fixtures to be created from id 1 on each test case. - This only works for auto-incrementing primary keys called "id" for now #359 *Scott Baron* - -* Added Base#clear_association_cache to empty all the cached associations #347 *Tobias Lütke* - -* Added more informative exceptions in establish_connection #356 *Jeremy Kemper* - -* Added Base#update_attributes that'll accept a hash of attributes and save the record (returning true if it passed validation, false otherwise). - - Before: - person.attributes = @params["person"] - person.save - - Now: - person.update_attributes(@params["person"]) - -* Added Base.destroy and Base.delete to remove records without holding a reference to them first. - -* Added that query benchmarking will only happen if its going to be logged anyway #344 - -* Added higher_item and lower_item as public methods for acts_as_list #342 *Tobias Lütke* - -* Fixed that options[:counter_sql] was overwritten with interpolated sql rather than original sql #355 *Jeremy Kemper* - -* Fixed that overriding an attribute's accessor would be disregarded by add_on_empty and add_on_boundary_breaking because they simply used - the attributes[] hash instead of checking for @base.respond_to?(attr.to_s). *Marten* - -* Fixed that Base.table_name would expect a parameter when used in has_and_belongs_to_many joins *Anna Lissa Cruz* - -* Fixed that nested transactions now work by letting the outer most transaction have the responsibilty of starting and rolling back the transaction. - If any of the inner transactions swallow the exception raised, though, the transaction will not be rolled back. So always let the transaction - bubble up even when you've dealt with local issues. Closes #231 and #340. - -* Fixed validates_{confirmation,acceptance}_of to only happen when the virtual attributes are not nil #348 *dpiddy@gmail.com* - -* Changed the interface on AbstractAdapter to require that adapters return the number of affected rows on delete and update operations. - -* Fixed the automated timestamping feature when running under Rails' development environment that resets the inheritable attributes on each request. - - - -## 1.2.0 ## - -* Added Base.validates_inclusion_of that validates whether the value of the specified attribute is available in a particular enumerable - object. *what-a-day* - - class Person < ActiveRecord::Base - validates_inclusion_of :gender, :in=>%w( m f ), :message=>"woah! what are you then!??!!" - validates_inclusion_of :age, :in=>0..99 - end - -* Added acts_as_list that can decorates an existing class with methods like move_higher/lower, move_to_top/bottom. [Tobias Lütke] Example: - - class TodoItem < ActiveRecord::Base - acts_as_list :scope => :todo_list_id - belongs_to :todo_list - end - -* Added acts_as_tree that can decorates an existing class with a many to many relationship with itself. Perfect for categories in - categories and the likes. *Tobias Lütke* - -* Added that Active Records will automatically record creation and/or update timestamps of database objects if fields of the names - created_at/created_on or updated_at/updated_on are present. *Tobias Lütke* - -* Added Base.default_error_messages as a hash of all the error messages used in the validates_*_of so they can be changed in one place *Tobias Lütke* - -* Added automatic transaction block around AssociationCollection.<<, AssociationCollection.delete, and AssociationCollection.destroy_all - -* Fixed that Base#find will return an array if given an array -- regardless of the number of elements #270 *Marten* - -* Fixed that has_and_belongs_to_many would generate bad sql when naming conventions differed from using vanilla "id" everywhere *RedTerror* - -* Added a better exception for when a type column is used in a table without the intention of triggering single-table inheritance. Example: - - ActiveRecord::SubclassNotFound: The single-table inheritance mechanism failed to locate the subclass: 'bad_class!'. - This error is raised because the column 'type' is reserved for storing the class in case of inheritance. - Please rename this column if you didn't intend it to be used for storing the inheritance class or - overwrite Company.inheritance_column to use another column for that information. - -* Added that single-table inheritance will only kick in if the inheritance_column (by default "type") is present. Otherwise, inheritance won't - have any magic side effects. - -* Added the possibility of marking fields as being in error without adding a message (using nil) to it that'll get displayed wth full_messages #208 *mjobin* - -* Fixed Base.errors to be indifferent as to whether strings or symbols are used. Examples: - - Before: - errors.add(:name, "must be shorter") if name.size > 10 - errors.on(:name) # => "must be shorter" - errors.on("name") # => nil - - After: - errors.add(:name, "must be shorter") if name.size > 10 - errors.on(:name) # => "must be shorter" - errors.on("name") # => "must be shorter" - -* Added Base.validates_format_of that Validates whether the value of the specified attribute is of the correct form by matching - it against the regular expression provided. *Marcel Molina Jr.* - - class Person < ActiveRecord::Base - validates_format_of :email, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/, :on => :create - end - -* Added Base.validates_length_of that delegates to add_on_boundary_breaking #312 [Tobias Lütke]. Example: - - Validates that the specified attribute matches the length restrictions supplied in either: - - - configuration[:minimum] - - configuration[:maximum] - - configuration[:is] - - configuration[:within] (aka. configuration[:in]) - - Only one option can be used at a time. - - class Person < ActiveRecord::Base - validates_length_of :first_name, :maximum=>30 - validates_length_of :last_name, :maximum=>30, :message=>"less than %d if you don't mind" - validates_length_of :user_name, :within => 6..20, :too_long => "pick a shorter name", :too_short => "pick a longer name" - validates_length_of :fav_bra_size, :minimum=>1, :too_short=>"please enter at least %d character" - validates_length_of :smurf_leader, :is=>4, :message=>"papa is spelled with %d characters... don't play me." - end - -* Added Base.validate_presence as an alternative to implementing validate and doing errors.add_on_empty yourself. - -* Added Base.validates_uniqueness_of that alidates 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 - - When the record is created, a check is performed to make sure that no record exist 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. - - -* Added Base.validates_confirmation_of that encapsulates the pattern of wanting to validate a password or email address field with a confirmation. Example: - - Model: - class Person < ActiveRecord::Base - validates_confirmation_of :password - end - - View: - <%= password_field "person", "password" %> - <%= password_field "person", "password_confirmation" %> - - The person has to already have a password attribute (a column in the people table), but the password_confirmation is virtual. - It exists only as an in-memory variable for validating the password. This check is performed both on create and update. - - -* Added Base.validates_acceptance_of that encapsulates the pattern of wanting to validate the acceptance of a terms of service check box (or similar agreement). Example: - - class Person < ActiveRecord::Base - validates_acceptance_of :terms_of_service - end - - The terms_of_service attribute is entirely virtual. No database column is needed. This check is performed both on create and update. - - NOTE: The agreement is considered valid if it's set to the string "1". This makes it easy to relate it to an HTML checkbox. - - -* Added validation macros to make the stackable just like the life cycle callbacks. Examples: - - class Person < ActiveRecord::Base - validate { |record| record.errors.add("name", "too short") unless name.size > 10 } - validate { |record| record.errors.add("name", "too long") unless name.size < 20 } - validate_on_create :validate_password - - private - def validate_password - errors.add("password", "too short") unless password.size > 6 - end - end - -* Added the option for sanitizing find_by_sql and the offset parts in regular finds [Sam Stephenson]. Examples: - - Project.find_all ["category = ?", category_name], "created ASC", ["? OFFSET ?", 15, 20] - Post.find_by_sql ["SELECT * FROM posts WHERE author = ? AND created > ?", author_id, start_date] - -* Fixed value quoting in all generated SQL statements, so that integers are not surrounded in quotes and that all sanitation are happening - through the database's own quoting routine. This should hopefully make it lots easier for new adapters that doesn't accept '1' for integer - columns. - -* Fixed has_and_belongs_to_many guessing of foreign key so that keys are generated correctly for models like SomeVerySpecialClient - *Florian Weber* - -* Added counter_sql option for has_many associations [Jeremy Kemper]. Documentation: - - <tt>:counter_sql</tt> - specify a complete SQL statement to fetch the size of the association. If +:finder_sql+ is - specified but +:counter_sql+, +:counter_sql+ will be generated by replacing SELECT ... FROM with SELECT COUNT(*) FROM. - -* Fixed that methods wrapped in callbacks still return their original result #260 *Jeremy Kemper* - -* Fixed the Inflector to handle the movie/movies pair correctly #261 *Scott Baron* - -* Added named bind-style variable interpolation #281 [Michael Koziarski]. Example: - - Person.find(["id = :id and first_name = :first_name", { :id => 5, :first_name = "bob' or 1=1" }]) - -* Added bind-style variable interpolation for the condition arrays that uses the adapter's quote method *Michael Koziarski* - - Before: - find_first([ "user_name = '%s' AND password = '%s'", user_name, password ])] - find_first([ "firm_id = %s", firm_id ])] # unsafe! - - After: - find_first([ "user_name = ? AND password = ?", user_name, password ])] - find_first([ "firm_id = ?", firm_id ])] - -* Added CSV format for fixtures #272 [what-a-day]. (See the new and expanded documentation on fixtures for more information) - -* Fixed fixtures using primary key fields called something else than "id" *dave* - -* Added proper handling of time fields that are turned into Time objects with the dummy date of 2000/1/1 *HariSeldon* - -* Added reverse order of deleting fixtures, so referential keys can be maintained #247 *Tim Bates* - -* Added relative path search for sqlite dbfiles in database.yml (if RAILS_ROOT is defined) #233 *Jeremy Kemper* - -* Added option to establish_connection where you'll be able to leave out the parameter to have it use the RAILS_ENV environment variable - -* Fixed problems with primary keys and postgresql sequences (#230) *Tim Bates* - -* Added reloading for associations under cached environments like FastCGI and mod_ruby. This makes it possible to use those environments for development. - This is turned on by default, but can be turned off with ActiveRecord::Base.reload_dependencies = false in production environments. - - NOTE: This will only have an effect if you let the associations manage the requiring of model classes. All libraries loaded through - require will be "forever" cached. You can, however, use ActiveRecord::Base.load_or_require("library") to get this behavior outside of the - auto-loading associations. - -* Added ERB capabilities to the fixture files for dynamic fixture generation. You don't need to do anything, just include ERB blocks like: - - david: - id: 1 - name: David - - jamis: - id: 2 - name: Jamis - - <% for digit in 3..10 %> - dev_<%= digit %>: - id: <%= digit %> - name: fixture_<%= digit %> - <% end %> - -* Changed the yaml fixture searcher to look in the root of the fixtures directory, so when you before could have something like: - - fixtures/developers/fixtures.yaml - fixtures/accounts/fixtures.yaml - - ...you now need to do: - - fixtures/developers.yaml - fixtures/accounts.yaml - -* Changed the fixture format from: - - name: david - data: - id: 1 - name: David Heinemeier Hansson - birthday: 1979-10-15 - profession: Systems development - --- - name: steve - data: - id: 2 - name: Steve Ross Kellock - birthday: 1974-09-27 - profession: guy with keyboard - - ...to: - - david: - id: 1 - name: David Heinemeier Hansson - birthday: 1979-10-15 - profession: Systems development - - steve: - id: 2 - name: Steve Ross Kellock - birthday: 1974-09-27 - profession: guy with keyboard - - The change is NOT backwards compatible. Fixtures written in the old YAML style needs to be rewritten! - -* All associations will now attempt to require the classes that they associate to. Relieving the need for most explicit 'require' statements. - - -## 1.1.0 (34) ## - -* Added automatic fixture setup and instance variable availability. Fixtures can also be automatically - instantiated in instance variables relating to their names using the following style: - - class FixturesTest < Test::Unit::TestCase - fixtures :developers # you can add more with comma separation - - def test_developers - assert_equal 3, @developers.size # the container for all the fixtures is automatically set - assert_kind_of Developer, @david # works like @developers["david"].find - assert_equal "David Heinemeier Hansson", @david.name - end - end - -* Added HasAndBelongsToManyAssociation#push_with_attributes(object, join_attributes) that can create associations in the join table with additional - attributes. This is really useful when you have information that's only relevant to the join itself, such as a "added_on" column for an association - between post and category. The added attributes will automatically be injected into objects retrieved through the association similar to the piggy-back - approach: - - post.categories.push_with_attributes(category, :added_on => Date.today) - post.categories.first.added_on # => Date.today - - NOTE: The categories table doesn't have an added_on column, it's the categories_post join table that does! - -* Fixed that :exclusively_dependent and :dependent can't be activated at the same time on has_many associations *Jeremy Kemper* - -* Fixed that database passwords couldn't be all numeric *Jeremy Kemper* - -* Fixed that calling id would create the instance variable for new_records preventing them from being saved correctly *Jeremy Kemper* - -* Added sanitization feature to HasManyAssociation#find_all so it works just like Base.find_all *Sam Stephenson/Jeremy Kemper* - -* Added that you can pass overlapping ids to find without getting duplicated records back *Jeremy Kemper* - -* Added that Base.benchmark returns the result of the block *Jeremy Kemper* - -* Fixed problem with unit tests on Windows with SQLite *paterno* - -* Fixed that quotes would break regular non-yaml fixtures *Dmitry Sabanin/daft* - -* Fixed fixtures on windows with line endings cause problems under unix / mac *Tobias Lütke* - -* Added HasAndBelongsToManyAssociation#find(id) that'll search inside the collection and find the object or record with that id - -* Added :conditions option to has_and_belongs_to_many that works just like the one on all the other associations - -* Added AssociationCollection#clear to remove all associations from has_many and has_and_belongs_to_many associations without destroying the records *geech* - -* Added type-checking and remove in 1-instead-of-N sql statements to AssociationCollection#delete *geech* - -* Added a return of self to AssociationCollection#<< so appending can be chained, like project << Milestone.create << Milestone.create *geech* - -* Added Base#hash and Base#eql? which means that all of the equality using features of array and other containers now works: - - [ Person.find(1), Person.find(2), Person.find(3) ] & [ Person.find(1), Person.find(4) ] # => [ Person.find(1) ] - -* Added :uniq as an option to has_and_belongs_to_many which will automatically ensure that AssociateCollection#uniq is called - before pulling records out of the association. This is especially useful for three-way (and above) has_and_belongs_to_many associations. - -* Added AssociateCollection#uniq which is especially useful for has_and_belongs_to_many associations that can include duplicates, - which is common on associations that also use metadata. Usage: post.categories.uniq - -* Fixed respond_to? to use a subclass specific hash instead of an Active Record-wide one - -* Fixed has_and_belongs_to_many to treat associations between classes in modules properly *Florian Weber* - -* Added a NoMethod exception to be raised when query and writer methods are called for attributes that doesn't exist *geech* - -* Added a more robust version of Fixtures that throws meaningful errors when on formatting issues *geech* - -* Added Base#transaction as a compliment to Base.transaction for prettier use in instance methods *geech* - -* Improved the speed of respond_to? by placing the dynamic methods lookup table in a hash *geech* - -* Added that any additional fields added to the join table in a has_and_belongs_to_many association - will be placed as attributes when pulling records out through has_and_belongs_to_many associations. - This is helpful when have information about the association itself that you want available on retrival. - -* Added better loading exception catching and RubyGems retries to the database adapters *alexeyv* - -* Fixed bug with per-model transactions *daniel* - -* Fixed Base#transaction so that it returns the result of the last expression in the transaction block *alexeyv* - -* Added Fixture#find to find the record corresponding to the fixture id. The record - class name is guessed by using Inflector#classify (also new) on the fixture directory name. - - Before: Document.find(@documents["first"]["id"]) - After : @documents["first"].find - -* Fixed that the table name part of column names ("TABLE.COLUMN") wasn't removed properly *Andreas Schwarz* - -* Fixed a bug with Base#size when a finder_sql was used that didn't capitalize SELECT and FROM *geech* - -* Fixed quoting problems on SQLite by adding quote_string to the AbstractAdapter that can be overwritten by the concrete - adapters for a call to the dbm. *Andreas Schwarz* - -* Removed RubyGems backup strategy for requiring SQLite-adapter -- if people want to use gems, they're already doing it with AR. - - -## 1.0.0 (35) ## - -* Added OO-style associations methods [Florian Weber]. Examples: - - Project#milestones_count => Project#milestones.size - Project#build_to_milestones => Project#milestones.build - Project#create_for_milestones => Project#milestones.create - Project#find_in_milestones => Project#milestones.find - Project#find_all_in_milestones => Project#milestones.find_all - -* Added serialize as a new class method to control when text attributes should be YAMLized or not. This means that automated - serialization of hashes, arrays, and so on WILL NO LONGER HAPPEN (#10). You need to do something like this: - - class User < ActiveRecord::Base - serialize :settings - end - - This will assume that settings is a text column and will now YAMLize any object put in that attribute. You can also specify - an optional :class_name option that'll raise an exception if a serialized object is retrieved as a descendant of a class not in - the hierarchy. Example: - - class User < ActiveRecord::Base - serialize :settings, :class_name => "Hash" - end - - user = User.create("settings" => %w( one two three )) - User.find(user.id).settings # => raises SerializationTypeMismatch - -* Added the option to connect to a different database for one model at a time. Just call establish_connection on the class - you want to have connected to another database than Base. This will automatically also connect decendents of that class - to the different database [Renald Buter]. - -* Added transactional protection for Base#save. Validations can now check for values knowing that it happens in a transaction and callbacks - can raise exceptions knowing that the save will be rolled back. *Suggested by Alexey Verkhovsky* - -* Added column name quoting so reserved words, such as "references", can be used as column names *Ryan Platte* - -* Added the possibility to chain the return of what happened inside a logged block [geech]: - - This now works: - log { ... }.map { ... } - - Instead of doing: - result = [] - log { result = ... } - result.map { ... } - -* Added "socket" option for the MySQL adapter, so you can change it to something else than "/tmp/mysql.sock" *Anna Lissa Cruz* - -* Added respond_to? answers for all the attribute methods. So if Person has a name attribute retrieved from the table schema, - person.respond_to? "name" will return true. - -* Added Base.benchmark which can be used to aggregate logging and benchmark, so you can measure and represent multiple statements in a single block. - Usage (hides all the SQL calls for the individual actions and calculates total runtime for them all): - - Project.benchmark("Creating project") do - project = Project.create("name" => "stuff") - project.create_manager("name" => "David") - project.milestones << Milestone.find_all - end - -* Added logging of invalid SQL statements *Daniel Von Fange* - -* Added alias Errors#[] for Errors#on, so you can now say person.errors["name"] to retrieve the errors for name *Andreas Schwarz* - -* Added RubyGems require attempt if sqlite-ruby is not available through regular methods. - -* Added compatibility with 2.x series of sqlite-ruby drivers. *Jamis Buck* - -* Added type safety for association assignments, so a ActiveRecord::AssociationTypeMismatch will be raised if you attempt to - assign an object that's not of the associated class. This cures the problem with nil giving id = 4 and fixnums giving id = 1 on - mistaken association assignments. *Reported by Andreas Schwarz* - -* Added the option to keep many fixtures in one single YAML document *what-a-day* - -* Added the class method "inheritance_column" that can be overwritten to return the name of an alternative column than "type" for storing - the type for inheritance hierarchies. *Dave Steinberg* - -* Added [] and []= as an alternative way to access attributes when the regular methods have been overwritten *Dave Steinberg* - -* Added the option to observer more than one class at the time by specifying observed_class as an array - -* Added auto-id propagation support for tables with arbitrary primary keys that have autogenerated sequences associated with them - on PostgreSQL. *Dave Steinberg* - -* Changed that integer and floats set to "" through attributes= remain as NULL. This was especially a problem for scaffolding and postgresql. (#49) - -* Changed the MySQL Adapter to rely on MySQL for its defaults for socket, host, and port *Andreas Schwarz* - -* Changed ActionControllerError to decent from StandardError instead of Exception. It can now be caught by a generic rescue. - -* Changed class inheritable attributes to not use eval *Caio Chassot* - -* Changed Errors#add to now use "invalid" as the default message instead of true, which means full_messages work with those *Marcel Molina Jr.* - -* Fixed spelling on Base#add_on_boundry_breaking to Base#add_on_boundary_breaking (old naming still works) *Marcel Molina Jr.* - -* Fixed that entries in the has_and_belongs_to_many join table didn't get removed when an associated object was destroyed. - -* Fixed unnecessary calls to SET AUTOCOMMIT=0/1 for MySQL adapter *Andreas Schwarz* - -* Fixed PostgreSQL defaults are now handled gracefully *Dave Steinberg* - -* Fixed increment/decrement_counter are now atomic updates *Andreas Schwarz* - -* Fixed the problems the Inflector had turning Attachment into attuchments and Cases into Casis *radsaq/Florian Gross* - -* Fixed that cloned records would point attribute references on the parent object *Andreas Schwarz* - -* Fixed SQL for type call on inheritance hierarchies *Caio Chassot* - -* Fixed bug with typed inheritance *Florian Weber* - -* Fixed a bug where has_many collection_count wouldn't use the conditions specified for that association - - -## 0.9.5 ## - -* Expanded the table_name guessing rules immensely [Florian Green]. Documentation: - - Guesses the table name (in forced lower-case) based on the name of the class in the inheritance hierarchy descending - directly from ActiveRecord. So if the hierarchy looks like: Reply < Message < ActiveRecord, then Message is used - to guess the table name from even when called on Reply. The guessing rules are as follows: - * Class name ends in "x", "ch" or "ss": "es" is appended, so a Search class becomes a searches table. - * Class name ends in "y" preceded by a consonant or "qu": The "y" is replaced with "ies", - so a Category class becomes a categories table. - * Class name ends in "fe": The "fe" is replaced with "ves", so a Wife class becomes a wives table. - * Class name ends in "lf" or "rf": The "f" is replaced with "ves", so a Half class becomes a halves table. - * Class name ends in "person": The "person" is replaced with "people", so a Salesperson class becomes a salespeople table. - * Class name ends in "man": The "man" is replaced with "men", so a Spokesman class becomes a spokesmen table. - * Class name ends in "sis": The "i" is replaced with an "e", so a Basis class becomes a bases table. - * Class name ends in "tum" or "ium": The "um" is replaced with an "a", so a Datum class becomes a data table. - * Class name ends in "child": The "child" is replaced with "children", so a NodeChild class becomes a node_children table. - * Class name ends in an "s": No additional characters are added or removed. - * Class name doesn't end in "s": An "s" is appended, so a Comment class becomes a comments table. - * Class name with word compositions: Compositions are underscored, so CreditCard class becomes a credit_cards table. - Additionally, the class-level table_name_prefix is prepended to the table_name and the table_name_suffix is appended. - So if you have "myapp_" as a prefix, the table name guess for an Account class becomes "myapp_accounts". - - You can also overwrite this class method to allow for unguessable links, such as a Mouse class with a link to a - "mice" table. Example: - - class Mouse < ActiveRecord::Base - def self.table_name() "mice" end - end - - This conversion is now done through an external class called Inflector residing in lib/active_record/support/inflector.rb. - -* Added find_all_in_collection to has_many defined collections. Works like this: - - class Firm < ActiveRecord::Base - has_many :clients - end - - firm.id # => 1 - firm.find_all_in_clients "revenue > 1000" # SELECT * FROM clients WHERE firm_id = 1 AND revenue > 1000 - - *Requested by Dave Thomas* - -* Fixed finders for inheritance hierarchies deeper than one level *Florian Weber* - -* Added add_on_boundry_breaking to errors to accompany add_on_empty as a default validation method. It's used like this: - - class Person < ActiveRecord::Base - protected - def validation - errors.add_on_boundry_breaking "password", 3..20 - end - end - - This will add an error to the tune of "is too short (minimum is 3 characters)" or "is too long (minimum is 20 characters)" if - the password is outside the boundry. The messages can be changed by passing a third and forth parameter as message strings. - -* Implemented a clone method that works properly with AR. It returns a clone of the record that - hasn't been assigned an id yet and is treated as a new record. - -* Allow for domain sockets in PostgreSQL by not assuming localhost when no host is specified *Scott Barron* - -* Fixed that bignums are saved properly instead of attempted to be YAMLized *Andreas Schwartz* - -* Fixed a bug in the GEM where the rdoc options weren't being passed according to spec *Chad Fowler* - -* Fixed a bug with the exclusively_dependent option for has_many - - -## 0.9.4 ## - -* Correctly guesses the primary key when the class is inside a module [Dave Steinberg]. - -* Added [] and []= as alternatives to read_attribute and write_attribute *Dave Steinberg* - -* has_and_belongs_to_many now accepts an :order key to determine in which order the collection is returned [radsaq]. - -* The ids passed to find and find_on_conditions are now automatically sanitized. - -* Added escaping of plings in YAML content. - -* Multi-parameter assigns where all the parameters are empty will now be set to nil instead of a new instance of their class. - -* Proper type within an inheritance hierarchy is now ensured already at object initialization (instead of first at create) - - -## 0.9.3 ## - -* Fixed bug with using a different primary key name together with has_and_belongs_to_many *Investigation by Scott* - -* Added :exclusively_dependent option to the has_many association macro. The doc reads: - - If set to true all the associated object are deleted in one SQL statement without having their - before_destroy callback run. This should only be used on associations that depend solely on - this class and don't need to do any clean-up in before_destroy. The upside is that it's much - faster, especially if there's a counter_cache involved. - -* Added :port key to connection options, so the PostgreSQL and MySQL adapters can connect to a database server - running on another port than the default. - -* Converted the new natural singleton methods that prevented AR objects from being saved by PStore - (and hence be placed in a Rails session) to a module. *Florian Weber* - -* Fixed the use of floats (was broken since 0.9.0+) - -* Fixed PostgreSQL adapter so default values are displayed properly when used in conjunction with - Action Pack scaffolding. - -* Fixed booleans support for PostgreSQL (use real true/false on boolean fields instead of 0/1 on tinyints) *radsaq* - - -## 0.9.2 ## - -* Added static method for instantly updating a record - -* Treat decimal and numeric as Ruby floats *Andreas Schwartz* - -* Treat chars as Ruby strings (fixes problem for Action Pack form helpers too) - -* Removed debugging output accidently left in (which would screw web applications) - - -## 0.9.1 ## - -* Added MIT license - -* Added natural object-style assignment for has_and_belongs_to_many associations. Consider the following model: - - class Event < ActiveRecord::Base - has_one_and_belongs_to_many :sponsors - end - - class Sponsor < ActiveRecord::Base - has_one_and_belongs_to_many :sponsors - end - - Earlier, you'd have to use synthetic methods for creating associations between two objects of the above class: - - roskilde_festival.add_to_sponsors(carlsberg) - roskilde_festival.remove_from_sponsors(carlsberg) - - nike.add_to_events(world_cup) - nike.remove_from_events(world_cup) - - Now you can use regular array-styled methods: - - roskilde_festival.sponsors << carlsberg - roskilde_festival.sponsors.delete(carlsberg) - - nike.events << world_cup - nike.events.delete(world_cup) - -* Added delete method for has_many associations. Using this will nullify an association between the has_many and the belonging - object by setting the foreign key to null. Consider this model: - - class Post < ActiveRecord::Base - has_many :comments - end - - class Comment < ActiveRecord::Base - belongs_to :post - end - - You could do something like: - - funny_comment.has_post? # => true - announcement.comments.delete(funny_comment) - funny_comment.has_post? # => false - - -## 0.9.0 ## - -* Active Record is now thread safe! (So you can use it with Cerise and WEBrick applications) - *Implementation idea by Michael Neumann, debugging assistance by Jamis Buck* - -* Improved performance by roughly 400% on a basic test case of pulling 100 records and querying one attribute. - This brings the tax for using Active Record instead of "riding on the metal" (using MySQL-ruby C-driver directly) down to ~50%. - Done by doing lazy type conversions and caching column information on the class-level. - -* Added callback objects and procs as options for implementing the target for callback macros. - -* Added "counter_cache" option to belongs_to that automates the usage of increment_counter and decrement_counter. Consider: - - class Post < ActiveRecord::Base - has_many :comments - end - - class Comment < ActiveRecord::Base - belongs_to :post - end - - Iterating over 100 posts like this: - - <% for post in @posts %> - <%= post.title %> has <%= post.comments_count %> comments - <% end %> - - Will generate 100 SQL count queries -- one for each call to post.comments_count. If you instead add a "comments_count" int column - to the posts table and rewrite the comments association macro with: - - class Comment < ActiveRecord::Base - belongs_to :post, :counter_cache => true - end - - Those 100 SQL count queries will be reduced to zero. Beware that counter caching is only appropriate for objects that begin life - with the object it's specified to belong with and is destroyed like that as well. Typically objects where you would also specify - :dependent => true. If your objects switch from one belonging to another (like a post that can be move from one category to another), - you'll have to manage the counter yourself. - -* Added natural object-style assignment for has_one and belongs_to associations. Consider the following model: - - class Project < ActiveRecord::Base - has_one :manager - end - - class Manager < ActiveRecord::Base - belongs_to :project - end - - Earlier, assignments would work like following regardless of which way the assignment told the best story: - - active_record.manager_id = david.id - - Now you can do it either from the belonging side: - - david.project = active_record - - ...or from the having side: - - active_record.manager = david - - If the assignment happens from the having side, the assigned object is automatically saved. So in the example above, the - project_id attribute on david would be set to the id of active_record, then david would be saved. - -* Added natural object-style assignment for has_many associations [Florian Weber]. Consider the following model: - - class Project < ActiveRecord::Base - has_many :milestones - end - - class Milestone < ActiveRecord::Base - belongs_to :project - end - - Earlier, assignments would work like following regardless of which way the assignment told the best story: - - deadline.project_id = active_record.id - - Now you can do it either from the belonging side: - - deadline.project = active_record - - ...or from the having side: - - active_record.milestones << deadline - - The milestone is automatically saved with the new foreign key. - -* API CHANGE: Attributes for text (or blob or similar) columns will now have unknown classes stored using YAML instead of using - to_s. (Known classes that won't be yamelized are: String, NilClass, TrueClass, FalseClass, Fixnum, Date, and Time). - Likewise, data pulled out of text-based attributes will be attempted converged using Yaml if they have the "--- " header. - This was primarily done to be enable the storage of hashes and arrays without wrapping them in aggregations, so now you can do: - - user = User.find(1) - user.preferences = { "background" => "black", "display" => large } - user.save - - User.find(1).preferences # => { "background" => "black", "display" => large } - - Please note that this method should only be used when you don't care about representing the object in proper columns in - the database. A money object consisting of an amount and a currency is still a much better fit for a value object done through - aggregations than this new option. - -* POSSIBLE CODE BREAKAGE: As a consequence of the lazy type conversions, it's a bad idea to reference the @attributes hash - directly (it always was, but now it's paramount that you don't). If you do, you won't get the type conversion. So to implement - new accessors for existing attributes, use read_attribute(attr_name) and write_attribute(attr_name, value) instead. Like this: - - class Song < ActiveRecord::Base - # Uses an integer of seconds to hold the length of the song - - def length=(minutes) - write_attribute("length", minutes * 60) - end - - def length - read_attribute("length") / 60 - end - end - - The clever kid will notice that this opens a door to sidestep the automated type conversion by using @attributes directly. - This is not recommended as read/write_attribute may be granted additional responsibilities in the future, but if you think - you know what you're doing and aren't afraid of future consequences, this is an option. - -* Applied a few minor bug fixes reported by Daniel Von Fange. - - -## 0.8.4 ## - - _Reflection_ -* Added ActiveRecord::Reflection with a bunch of methods and classes for reflecting in aggregations and associations. - -* Added Base.columns and Base.content_columns which returns arrays of column description (type, default, etc) objects. - -* Added Base#attribute_names which returns an array of names for the attributes available on the object. - -* Added Base#column_for_attribute(name) which returns the column description object for the named attribute. - - - _Misc_ -* Added multi-parameter assignment: - - # Instantiate 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 - # written_on (a date type) with Date.new("2004", "6", "24"). You can also specify a typecast character in the - # parenteses to have the parameters typecasted before they're used in the constructor. Use i for Fixnum, f for Float, - # s for String, and a for Array. - - This is incredibly useful for assigning dates from HTML drop-downs of month, year, and day. - -* Fixed bug with custom primary key column name and Base.find on multiple parameters. - -* Fixed bug with dependent option on has_one associations if there was no associated object. - - -## 0.8.3 ## - - _Transactions_ -* Added transactional protection for destroy (important for the new :dependent option) *Suggested by Carl Youngblood* - -* Fixed so transactions are ignored on MyISAM tables for MySQL (use InnoDB to get transactions) - -* Changed transactions so only exceptions will cause a rollback, not returned false. - - - _Mapping_ -* Added support for non-integer primary keys *Aredridel/earlier work by Michael Neumann* - - User.find "jdoe" - Product.find "PDKEY-INT-12" - -* Added option to specify naming method for primary key column. ActiveRecord::Base.primary_key_prefix_type can either - be set to nil, :table_name, or :table_name_with_underscore. :table_name will assume that Product class has a primary key - of "productid" and :table_name_with_underscore will assume "product_id". The default nil will just give "id". - -* Added an overwriteable primary_key method that'll instruct AR to the name of the - id column *Aredridele/earlier work by Guan Yang* - - class Project < ActiveRecord::Base - def self.primary_key() "project_id" end - end - -* Fixed that Active Records can safely associate inside and out of modules. - - class MyApplication::Account < ActiveRecord::Base - has_many :clients # will look for MyApplication::Client - has_many :interests, :class_name => "Business::Interest" # will look for Business::Interest - end - -* Fixed that Active Records can safely live inside modules *Aredridel* - - class MyApplication::Account < ActiveRecord::Base - end - - - _Misc_ -* Added freeze call to value object assignments to ensure they remain immutable *Spotted by Gavin Sinclair* - -* Changed interface for specifying observed class in observers. Was OBSERVED_CLASS constant, now is - observed_class() class method. This is more consistant with things like self.table_name(). Works like this: - - class AuditObserver < ActiveRecord::Observer - def self.observed_class() Account end - def after_update(account) - AuditTrail.new(account, "UPDATED") - end - end - - *Suggested by Gavin Sinclair* - -* Create new Active Record objects by setting the attributes through a block. Like this: - - person = Person.new do |p| - p.name = 'Freddy' - p.age = 19 - end - - *Suggested by Gavin Sinclair* - - -## 0.8.2 ## - -* Added inheritable callback queues that can ensure that certain callback methods or inline fragments are - run throughout the entire inheritance hierarchy. Regardless of whether a descendant overwrites the callback - method: - - class Topic < ActiveRecord::Base - before_destroy :destroy_author, 'puts "I'm an inline fragment"' - end - - Learn more in link:classes/ActiveRecord/Callbacks.html - -* Added :dependent option to has_many and has_one, which will automatically destroy associated objects when - the holder is destroyed: - - class Album < ActiveRecord::Base - has_many :tracks, :dependent => true - end - - All the associated tracks are destroyed when the album is. - -* Added Base.create as a factory that'll create, save, and return a new object in one step. - -* Automatically convert strings in config hashes to symbols for the _connection methods. This allows you - to pass the argument hashes directly from yaml. (Luke) - -* Fixed the install.rb to include simple.rb *Spotted by Kevin Bullock* - -* Modified block syntax to better follow our code standards outlined in - http://www.rubyonrails.org/CodingStandards - - -## 0.8.1 ## - -* Added object-level transactions *Austin Ziegler* - -* Changed adapter-specific connection methods to use centralized ActiveRecord::Base.establish_connection, - which is parametized through a config hash with symbol keys instead of a regular parameter list. - This will allow for database connections to be opened in a more generic fashion. (Luke) - - NOTE: This requires all *_connections to be updated! Read more in: - http://ar.rubyonrails.org/classes/ActiveRecord/Base.html#M000081 - -* Fixed SQLite adapter so objects fetched from has_and_belongs_to_many have proper attributes - (t.name is now name). *Spotted by Garrett Rooney* - -* Fixed SQLite adapter so dates are returned as Date objects, not Time objects *Spotted by Gavin Sinclair* - -* Fixed requirement of date class, so date conversions are succesful regardless of whether you - manually require date or not. - - -## 0.8.0 ## +## Rails 3.2.4 (May 31, 2012) ## -* Added transactions +* Perf fix: Don't load the records when doing assoc.delete_all. + GH #6289. *Jon Leighton* -* Changed Base.find to also accept either a list (1, 5, 6) or an array of ids ([5, 7]) - as parameter and then return an array of objects instead of just an object +* Association preloading shouldn't be affected by the current scoping. + This could cause infinite recursion and potentially other problems. + See GH #5667. *Jon Leighton* -* Fixed method has_collection? for has_and_belongs_to_many macro to behave as a - collection, not an association +* Datetime attributes are forced to be changed. GH #3965 -* Fixed SQLite adapter so empty or nil values in columns of datetime, date, or time type - aren't treated as current time *Spotted by Gavin Sinclair* +* Fix attribute casting. GH #5549 +* Fix #5667. Preloading should ignore scoping. -## 0.7.6 ## +* Predicate builder should not recurse for determining where columns. + Thanks to Ben Murphy for reporting this! CVE-2012-2661 -* Fixed the install.rb to create the lib/active_record/support directory *Spotted by Gavin Sinclair* -* Fixed that has_association? would always return true *Daniel Von Fange* +Please check [3-2-stable](https://github.com/rails/rails/blob/3-2-stable/activerecord/CHANGELOG.md) for previous changes. diff --git a/activerecord/README.rdoc b/activerecord/README.rdoc index 60965590a1..d080e0b0f5 100644 --- a/activerecord/README.rdoc +++ b/activerecord/README.rdoc @@ -46,6 +46,18 @@ A short rundown of some of the major features: {Learn more}[link:classes/ActiveRecord/Associations/ClassMethods.html] +* Aggregations of value objects. + + class Account < ActiveRecord::Base + composed_of :balance, :class_name => "Money", + :mapping => %w(balance amount) + composed_of :address, + :mapping => [%w(address_street street), %w(address_city city)] + end + + {Learn more}[link:classes/ActiveRecord/Aggregations/ClassMethods.html] + + * Validation rules that can differ for new or existing objects. class Account < ActiveRecord::Base 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/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 a894d83ea7..1675127ab0 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -22,20 +22,54 @@ #++ 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 + 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' autoload :ConnectionAdapters, 'active_record/connection_adapters/abstract_adapter' + autoload :Aggregations autoload :Associations autoload :AttributeMethods autoload :AttributeAssignment @@ -51,43 +85,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 @@ -148,6 +149,15 @@ module ActiveRecord 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 new file mode 100644 index 0000000000..3db8e0716b --- /dev/null +++ b/activerecord/lib/active_record/aggregations.rb @@ -0,0 +1,261 @@ +module ActiveRecord + # = Active Record Aggregations + module Aggregations # :nodoc: + extend ActiveSupport::Concern + + def clear_aggregation_cache #:nodoc: + @aggregation_cache.clear if persisted? + end + + # Active Record implements aggregation through a macro-like class method called +composed_of+ + # for representing attributes as value objects. It expresses relationships like "Account [is] + # composed of Money [among other things]" or "Person [is] composed of [an] address". Each call + # to the macro adds a description of how the value objects are created from the attributes of + # the entity object (when the entity is initialized either as a new object or from finding an + # existing object) and how it can be turned back into attributes (when the entity is saved to + # the database). + # + # class Customer < ActiveRecord::Base + # composed_of :balance, :class_name => "Money", :mapping => %w(balance amount) + # composed_of :address, :mapping => [ %w(address_street street), %w(address_city city) ] + # end + # + # The customer class now has the following methods to manipulate the value objects: + # * <tt>Customer#balance, Customer#balance=(money)</tt> + # * <tt>Customer#address, Customer#address=(address)</tt> + # + # These methods will operate with value objects like the ones described below: + # + # class Money + # include Comparable + # attr_reader :amount, :currency + # EXCHANGE_RATES = { "USD_TO_DKK" => 6 } + # + # def initialize(amount, currency = "USD") + # @amount, @currency = amount, currency + # end + # + # def exchange_to(other_currency) + # exchanged_amount = (amount * EXCHANGE_RATES["#{currency}_TO_#{other_currency}"]).floor + # Money.new(exchanged_amount, other_currency) + # end + # + # def ==(other_money) + # amount == other_money.amount && currency == other_money.currency + # end + # + # def <=>(other_money) + # if currency == other_money.currency + # amount <=> other_money.amount + # else + # amount <=> other_money.exchange_to(currency).amount + # end + # end + # end + # + # class Address + # attr_reader :street, :city + # def initialize(street, city) + # @street, @city = street, city + # end + # + # def close_to?(other_address) + # city == other_address.city + # end + # + # def ==(other_address) + # city == other_address.city && street == other_address.street + # end + # end + # + # Now it's possible to access attributes from the database through the value objects instead. If + # you choose to name the composition the same as the attribute's name, it will be the only way to + # access that attribute. That's the case with our +balance+ attribute. You interact with the value + # objects just like you would with any other attribute: + # + # customer.balance = Money.new(20) # sets the Money value object and the attribute + # customer.balance # => Money value object + # customer.balance.exchange_to("DKK") # => Money.new(120, "DKK") + # customer.balance > Money.new(10) # => true + # customer.balance == Money.new(20) # => true + # customer.balance < Money.new(5) # => false + # + # Value objects can also be composed of multiple attributes, such as the case of Address. The order + # of the mappings will determine the order of the parameters. + # + # customer.address_street = "Hyancintvej" + # customer.address_city = "Copenhagen" + # customer.address # => Address.new("Hyancintvej", "Copenhagen") + # + # customer.address_street = "Vesterbrogade" + # customer.address # => Address.new("Hyancintvej", "Copenhagen") + # customer.clear_aggregation_cache + # customer.address # => Address.new("Vesterbrogade", "Copenhagen") + # + # customer.address = Address.new("May Street", "Chicago") + # customer.address_street # => "May Street" + # customer.address_city # => "Chicago" + # + # == Writing value objects + # + # Value objects are immutable and interchangeable objects that represent a given value, such as + # a Money object representing $5. Two Money objects both representing $5 should be equal (through + # methods such as <tt>==</tt> and <tt><=></tt> from Comparable if ranking makes sense). This is + # unlike entity objects where equality is determined by identity. An entity class such as Customer can + # easily have two different objects that both have an address on Hyancintvej. Entity identity is + # determined by object or relational unique identifiers (such as primary keys). Normal + # ActiveRecord::Base classes are entity objects. + # + # It's also important to treat the value objects as immutable. Don't allow the Money object to have + # its amount changed after creation. Create a new Money object with the new value instead. The + # Money#exchange_to method is an example of this. It returns a new value object instead of changing + # its own values. Active Record won't persist value objects that have been changed through means + # other than the writer method. + # + # The immutable requirement is enforced by Active Record by freezing any object assigned as a value + # object. Attempting to change it afterwards will result in a ActiveSupport::FrozenObjectError. + # + # Read more about value objects on http://c2.com/cgi/wiki?ValueObject and on the dangers of not + # keeping value objects immutable on http://c2.com/cgi/wiki?ValueObjectsShouldBeImmutable + # + # == Custom constructors and converters + # + # By default value objects are initialized by calling the <tt>new</tt> constructor of the value + # class passing each of the mapped attributes, in the order specified by the <tt>:mapping</tt> + # option, as arguments. If the value class doesn't support this convention then +composed_of+ allows + # a custom constructor to be specified. + # + # When a new value is assigned to the value object, the default assumption is that the new value + # is an instance of the value class. Specifying a custom converter allows the new value to be automatically + # converted to an instance of value class if necessary. + # + # For example, the NetworkResource model has +network_address+ and +cidr_range+ attributes that + # should be aggregated using the NetAddr::CIDR value class (http://netaddr.rubyforge.org). The constructor + # for the value class is called +create+ and it expects a CIDR address string as a parameter. New + # values can be assigned to the value object using either another NetAddr::CIDR object, a string + # or an array. The <tt>:constructor</tt> and <tt>:converter</tt> options can be used to meet + # these requirements: + # + # class NetworkResource < ActiveRecord::Base + # composed_of :cidr, + # :class_name => 'NetAddr::CIDR', + # :mapping => [ %w(network_address network), %w(cidr_range bits) ], + # :allow_nil => true, + # :constructor => Proc.new { |network_address, cidr_range| NetAddr::CIDR.create("#{network_address}/#{cidr_range}") }, + # :converter => Proc.new { |value| NetAddr::CIDR.create(value.is_a?(Array) ? value.join('/') : value) } + # end + # + # # This calls the :constructor + # network_resource = NetworkResource.new(:network_address => '192.168.0.1', :cidr_range => 24) + # + # # These assignments will both use the :converter + # network_resource.cidr = [ '192.168.2.1', 8 ] + # network_resource.cidr = '192.168.0.1/24' + # + # # This assignment won't use the :converter as the value is already an instance of the value class + # network_resource.cidr = NetAddr::CIDR.create('192.168.2.1/8') + # + # # Saving and then reloading will use the :constructor on reload + # network_resource.save + # network_resource.reload + # + # == Finding records by a value object + # + # Once a +composed_of+ relationship is specified for a model, records can be loaded from the database + # by specifying an instance of the value object in the conditions hash. The following example + # finds all customers with +balance_amount+ equal to 20 and +balance_currency+ equal to "USD": + # + # Customer.where(:balance => Money.new(20, "USD")).all + # + module ClassMethods + # Adds reader and writer methods for manipulating a value object: + # <tt>composed_of :address</tt> adds <tt>address</tt> and <tt>address=(new_address)</tt> methods. + # + # Options are: + # * <tt>:class_name</tt> - Specifies the class name of the association. Use it only if that name + # can't be inferred from the part id. So <tt>composed_of :address</tt> will by default be linked + # to the Address class, but if the real class name is CompanyAddress, you'll have to specify it + # with this option. + # * <tt>:mapping</tt> - Specifies the mapping of entity attributes to attributes of the value + # object. Each mapping is represented as an array where the first item is the name of the + # entity attribute and the second item is the name of the attribute in the value object. The + # order in which mappings are defined determines the order in which attributes are sent to the + # value class constructor. + # * <tt>:allow_nil</tt> - Specifies that the value object will not be instantiated when all mapped + # attributes are +nil+. Setting the value object to +nil+ has the effect of writing +nil+ to all + # mapped attributes. + # This defaults to +false+. + # * <tt>:constructor</tt> - A symbol specifying the name of the constructor method or a Proc that + # is called to initialize the value object. The constructor is passed all of the mapped attributes, + # in the order that they are defined in the <tt>:mapping option</tt>, as arguments and uses them + # to instantiate a <tt>:class_name</tt> object. + # The default is <tt>:new</tt>. + # * <tt>:converter</tt> - A symbol specifying the name of a class method of <tt>:class_name</tt> + # or a Proc that is called when a new value is assigned to the value object. The converter is + # passed the single value that is used in the assignment and is only called if the new value is + # not an instance of <tt>:class_name</tt>. If <tt>:allow_nil</tt> is set to true, the converter + # can return nil to skip the assignment. + # + # Option examples: + # composed_of :temperature, :mapping => %w(reading celsius) + # composed_of :balance, :class_name => "Money", :mapping => %w(balance amount), + # :converter => Proc.new { |balance| balance.to_money } + # composed_of :address, :mapping => [ %w(address_street street), %w(address_city city) ] + # composed_of :gps_location + # composed_of :gps_location, :allow_nil => true + # composed_of :ip_address, + # :class_name => 'IPAddr', + # :mapping => %w(ip to_i), + # :constructor => Proc.new { |ip| IPAddr.new(ip, Socket::AF_INET) }, + # :converter => Proc.new { |ip| ip.is_a?(Integer) ? IPAddr.new(ip, Socket::AF_INET) : IPAddr.new(ip.to_s) } + # + def composed_of(part_id, options = {}) + options.assert_valid_keys(:class_name, :mapping, :allow_nil, :constructor, :converter) + + name = part_id.id2name + class_name = options[:class_name] || name.camelize + mapping = options[:mapping] || [ name, name ] + mapping = [ mapping ] unless mapping.first.is_a?(Array) + allow_nil = options[:allow_nil] || false + constructor = options[:constructor] || :new + converter = options[:converter] + + reader_method(name, class_name, mapping, allow_nil, constructor) + writer_method(name, class_name, mapping, allow_nil, converter) + + create_reflection(:composed_of, part_id, nil, options, self) + end + + private + def reader_method(name, class_name, mapping, allow_nil, constructor) + define_method(name) do + if @aggregation_cache[name].nil? && (!allow_nil || mapping.any? {|pair| !read_attribute(pair.first).nil? }) + attrs = mapping.collect {|pair| read_attribute(pair.first)} + object = constructor.respond_to?(:call) ? + constructor.call(*attrs) : + class_name.constantize.send(constructor, *attrs) + @aggregation_cache[name] = object + end + @aggregation_cache[name] + end + end + + def writer_method(name, class_name, mapping, allow_nil, converter) + define_method("#{name}=") do |part| + klass = class_name.constantize + unless part.is_a?(klass) || converter.nil? || part.nil? + part = converter.respond_to?(:call) ? converter.call(part) : klass.send(converter, part) + end + + if part.nil? && allow_nil + mapping.each { |pair| self[pair.first] = nil } + @aggregation_cache[name] = nil + else + mapping.each { |pair| self[pair.first] = part.send(pair.last) } + @aggregation_cache[name] = part.freeze + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index a62fce4756..d6b8552e6e 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. + # By default, only save associated objects that are new records. This option is implemented as a + # before_save callback. Because callbacks are run in the order they are defined, associated objects + # may need to be explicitly saved in any user-defined before_save callbacks. # # Note that <tt>accepts_nested_attributes_for</tt> sets <tt>:autosave</tt> to <tt>true</tt>. # [:inverse_of] @@ -1176,24 +1149,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 @@ -1241,34 +1206,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 @@ -1282,8 +1236,6 @@ 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] @@ -1302,14 +1254,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 @@ -1354,13 +1306,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> @@ -1393,14 +1338,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] @@ -1421,18 +1362,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 @@ -1544,47 +1485,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] @@ -1599,14 +1499,12 @@ module ActiveRecord # # 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 f7656ecd47..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,8 +1,12 @@ 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 @@ -10,18 +14,26 @@ module ActiveRecord::Associations::Builder 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 - }) + [: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 end + 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 2f6ddfeeb3..b15df4f308 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -50,7 +50,7 @@ module ActiveRecord end else column = "#{reflection.quoted_table_name}.#{reflection.association_primary_key}" - scoped.pluck(column) + scope.pluck(column) end end @@ -71,7 +71,7 @@ module ActiveRecord if block_given? load_target.select.each { |e| yield e } else - scoped.select(select) + scope.select(select) end end @@ -82,7 +82,7 @@ module ActiveRecord if options[:finder_sql] find_by_scan(*args) else - scoped.find(*args) + scope.find(*args) end end end @@ -164,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 @@ -183,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[:distinct] = true end - value = scoped.count(column_name, count_options) + value = scope.count(column_name, count_options) limit = options[:limit] offset = options[:offset] @@ -246,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 @@ -270,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. @@ -298,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 @@ -324,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 @@ -344,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 @@ -380,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 @@ -441,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) @@ -566,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_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 f0d1120c68..06bead41de 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 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 + + else + delete + end + end + 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? - end + # 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 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}." + 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_column(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 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/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_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 5b41f72e52..d9989274c8 100644 --- a/activerecord/lib/active_record/attribute_assignment.rb +++ b/activerecord/lib/active_record/attribute_assignment.rb @@ -1,4 +1,3 @@ -require 'active_support/concern' module ActiveRecord ActiveSupport.on_load(:active_record_config) do @@ -85,11 +84,11 @@ module ActiveRecord def assign_attributes(new_attributes, options = {}) return if new_attributes.blank? - attributes = new_attributes.stringify_keys - multi_parameter_attributes = [] + attributes = new_attributes.stringify_keys + multi_parameter_attributes = [] nested_parameter_attributes = [] - previous_options = @mass_assignment_options - @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) @@ -98,23 +97,15 @@ 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 - - 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 @@ -131,8 +122,23 @@ 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 object with these parameters. + # 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 # written_on (a date type) with Date.new("2004", "6", "24"). You can also specify a typecast character in the # parentheses to have the parameters typecasted before they're used in the constructor. Use i for Fixnum, @@ -144,19 +150,11 @@ 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.message})", ex, name) end @@ -167,74 +165,12 @@ module ActiveRecord end end - def read_value_from_parameter(name, values_hash_from_param) - klass = 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 column is a :time (and not :date or :timestamp) there is no need to validate if - # there are year/month/day fields - if column_for_attribute(name).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_hash_from_param[key] ||= value - end - else - # else column is a timestamp, so if Date bits were not provided, error - if missing_parameter = [1,2,3].detect{ |position| !values_hash_from_param.has_key?(position) } - raise ArgumentError.new("Missing Parameter - #{name}(#{missing_parameter}i)") - end - - # If Date bits were provided but blank, then return nil - return nil if (1..3).any? { |position| values_hash_from_param[position].blank? } - end - - max_position = extract_max_param_for_multiparameter_attributes(values_hash_from_param, 6) - 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 - 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 @@ -251,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 f36df4a444..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 diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index a24b4b7839..60e5b0e2bb 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -1,5 +1,3 @@ -require 'active_support/core_ext/class/attribute' -require 'active_support/core_ext/object/blank' require 'active_support/core_ext/module/attribute_accessors' module ActiveRecord @@ -98,7 +96,7 @@ module ActiveRecord def changes_from_zero_to_string?(old, value) # For columns with old 0 and value non-empty string - old == 0 && value.present? && value != '0' + old == 0 && value.is_a?(String) && value.present? && value != '0' 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 a7af086e43..1a4cb25dd7 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -8,7 +8,6 @@ module ActiveRecord 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 @@ -46,7 +45,7 @@ module ActiveRecord def define_method_attribute(attr_name) generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 def __temp__ - read_attribute('#{attr_name}') { |n| missing_attribute(n, caller) } + read_attribute(:'#{attr_name}') { |n| missing_attribute(n, caller) } end alias_method '#{attr_name}', :__temp__ undef_method :__temp__ @@ -64,14 +63,22 @@ module ActiveRecord end end + ActiveRecord::Model.attribute_types_cached_by_default = ATTRIBUTE_TYPES_CACHED_BY_DEFAULT + # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example, # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)). def read_attribute(attr_name) + return unless attr_name + name_sym = attr_name.to_sym + # If it's cached, just return it - @attributes_cache.fetch(attr_name.to_s) { |name| + # We use #[] first as a perf optimization for non-nil values. See https://gist.github.com/3552829. + @attributes_cache[name_sym] || @attributes_cache.fetch(name_sym) { + name = attr_name.to_s + column = @columns_hash.fetch(name) { return @attributes.fetch(name) { - if name == 'id' && self.class.primary_key != name + if name_sym == :id && self.class.primary_key != name read_attribute(self.class.primary_key) end } @@ -82,7 +89,7 @@ module ActiveRecord } if self.class.cache_attribute?(name) - @attributes_cache[name] = column.type_cast(value) + @attributes_cache[name_sym] = column.type_cast(value) else column.type_cast value end diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb index 49ab3ab808..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. - class_attribute :serialized_attributes, instance_writer: false + 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 e300c9721f..9647d03be4 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -1,5 +1,3 @@ -require 'active_support/core_ext/class/attribute' -require 'active_support/core_ext/object/inclusion' module ActiveRecord ActiveSupport.on_load(:active_record_config) do @@ -61,11 +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 - changed = read_attribute(:#{attr_name}) != time - write_attribute(:#{attr_name}, original_time) - #{attr_name}_will_change! if changed - @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) @@ -81,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/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index 50435921b1..5a39cb0125 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -25,13 +25,13 @@ module ActiveRecord def write_attribute(attr_name, value) attr_name = attr_name.to_s attr_name = self.class.primary_key if attr_name == 'id' && self.class.primary_key - @attributes_cache.delete(attr_name) + @attributes_cache.delete(attr_name.to_sym) column = column_for_attribute(attr_name) # If we're dealing with a binary column, write the data to the cache # so we don't attempt to typecast multiple times. if column && column.binary? - @attributes_cache[attr_name] = value + @attributes_cache[attr_name.to_sym] = value end if column || @attributes.has_key?(attr_name) 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/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index 347d794fa3..7a3d9bfd3e 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -496,40 +496,40 @@ module ActiveRecord # ActiveRecord::Base.connection_handler. Active Record models use this to # determine that connection pool that they should use. class ConnectionHandler - def initialize(pools = Hash.new { |h,k| h[k] = {} }) - @connection_pools = pools - @class_to_pool = Hash.new { |h,k| h[k] = {} } + def initialize + @owner_to_pool = Hash.new { |h,k| h[k] = {} } + @class_to_pool = Hash.new { |h,k| h[k] = {} } end def connection_pools - @connection_pools[Process.pid] + owner_to_pool.values.compact end - def establish_connection(name, spec) - set_pool_for_spec spec, ConnectionAdapters::ConnectionPool.new(spec) - set_class_to_pool name, connection_pools[spec] + def establish_connection(owner, spec) + @class_to_pool.clear + owner_to_pool[owner] = ConnectionAdapters::ConnectionPool.new(spec) end # Returns true if there are any active connections among the connection # pools that the ConnectionHandler is managing. def active_connections? - connection_pools.values.any? { |pool| pool.active_connection? } + connection_pools.any?(&:active_connection?) end # Returns any connections in use by the current thread back to the pool, # and also returns connections to the pool cached by threads that are no # longer alive. def clear_active_connections! - connection_pools.each_value {|pool| pool.release_connection } + connection_pools.each(&:release_connection) end # Clears the cache which maps classes. def clear_reloadable_connections! - connection_pools.each_value {|pool| pool.clear_reloadable_connections! } + connection_pools.each(&:clear_reloadable_connections!) end def clear_all_connections! - connection_pools.each_value {|pool| pool.disconnect! } + connection_pools.each(&:disconnect!) end # Locate the connection of the nearest super class. This can be an @@ -552,56 +552,62 @@ module ActiveRecord # connection and the defined connection (if they exist). The result # can be used as an argument for establish_connection, for easily # re-establishing the connection. - def remove_connection(klass) - pool = class_to_pool.delete(klass.name) - return nil unless pool - - connection_pools.delete pool.spec - pool.automatic_reconnect = false - pool.disconnect! - pool.spec.config + def remove_connection(owner) + if pool = owner_to_pool.delete(owner) + @class_to_pool.clear + pool.automatic_reconnect = false + pool.disconnect! + pool.spec.config + end end + # Retrieving the connection pool happens a lot so we cache it in @class_to_pool. + # This makes retrieving the connection pool O(1) once the process is warm. + # When a connection is established or removed, we invalidate the cache. + # + # Ideally we would use #fetch here, as class_to_pool[klass] may sometimes be nil. + # However, benchmarking (https://gist.github.com/3552829) showed that #fetch is + # significantly slower than #[]. So in the nil case, no caching will take place, + # but that's ok since the nil case is not the common one that we wish to optimise + # for. def retrieve_connection_pool(klass) - 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) + class_to_pool[klass] ||= begin + until pool = pool_for(klass) + klass = klass.superclass + break unless klass < Model::Tag + end + + class_to_pool[klass] = pool || pool_for(ActiveRecord::Model) end end private - def class_to_pool - @class_to_pool[Process.pid] - end - - def set_pool_for_spec(spec, pool) - @connection_pools[Process.pid][spec] = pool + def owner_to_pool + @owner_to_pool[Process.pid] end - def set_class_to_pool(name, pool) - @class_to_pool[Process.pid][name] = pool - pool + def class_to_pool + @class_to_pool[Process.pid] end - def get_pool_for_class(klass) - @class_to_pool[Process.pid].fetch(klass) { - c_to_p = @class_to_pool.values.find { |class_to_pool| - class_to_pool[klass] - } - - if c_to_p - pool = c_to_p[klass] - pool = ConnectionAdapters::ConnectionPool.new pool.spec - set_pool_for_spec pool.spec, pool - set_class_to_pool klass, pool + def pool_for(owner) + owner_to_pool.fetch(owner) { + if ancestor_pool = pool_from_any_process_for(owner) + # A connection was established in an ancestor process that must have + # subsequently forked. We can't reuse the connection, but we can copy + # the specification and establish a new connection with it. + establish_connection owner, ancestor_pool.spec else - set_class_to_pool klass, nil + owner_to_pool[owner] = nil end } end + + def pool_from_any_process_for(owner) + owner_to_pool = @owner_to_pool.values.find { |v| v[owner] } + owner_to_pool && owner_to_pool[owner] + end end class ConnectionManagement 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 b0b51f540c..4e1f0e1d62 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,31 +173,23 @@ module ActiveRecord def transaction(options = {}) options.assert_valid_keys :requires_new, :joinable - last_transaction_joinable = defined?(@transaction_joinable) ? @transaction_joinable : nil - if options.has_key?(:joinable) - @transaction_joinable = options[:joinable] - else - @transaction_joinable = true - end - requires_new = options[:requires_new] || !last_transaction_joinable - - transaction_open = false - @_current_transaction_records ||= [] + last_transaction_joinable = @transaction_joinable + @transaction_joinable = options.fetch(:joinable, true) + requires_new = options[:requires_new] || !last_transaction_joinable + transaction_open = false begin - if block_given? - if requires_new || open_transactions == 0 - if open_transactions == 0 - begin_db_transaction - elsif requires_new - create_savepoint - end - increment_open_transactions - transaction_open = true - @_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 +223,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) 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 a6e16da730..be6fda95b4 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb @@ -65,6 +65,7 @@ module ActiveRecord end private + def cache_sql(sql, binds) result = if @query_cache[sql].key?(binds) @@ -85,11 +86,7 @@ module ActiveRecord end def locked?(arel) - if arel.respond_to?(:locked) - arel.locked - else - false - end + arel.respond_to?(:locked) && arel.locked end end end 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 ef17dfbbc5..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' @@ -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 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 65c7ef0153..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,7 +63,10 @@ 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) && @@ -202,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 @@ -485,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 diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 28a9821913..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' 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 df4a9d5afc..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 @@ -318,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/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_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 7b263fd62d..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' @@ -474,6 +473,7 @@ module ActiveRecord def reconnect! clear_cache! @connection.reset + @open_transactions = 0 configure_connection end @@ -803,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) : @@ -825,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 @@ -1227,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. @@ -1374,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 diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 57aa47ab61..4fe0013f0f 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -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 bda41df80f..3531be05bf 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 @@ -45,7 +44,7 @@ module ActiveRecord end remove_connection - connection_handler.establish_connection name, spec + connection_handler.establish_connection self, spec end # Returns the connection currently associated with the class. This can diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 90f156456e..aad21b8e37 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -1,7 +1,5 @@ -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/module/delegation' +require 'active_support/core_ext/object/duplicable' require 'thread' module ActiveRecord @@ -84,15 +82,6 @@ module ActiveRecord # The connection handler config_attribute :connection_handler - ## - # :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 - %w(logger configurations default_timezone schema_format timestamped_migrations).each do |name| config_attribute name, global: true end @@ -184,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 @@ -196,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 @@ -266,6 +258,7 @@ module ActiveRecord @changed_attributes[attr] = orig_value if _field_changed?(attr, orig_value, @attributes[attr]) end + @aggregation_cache = {} @association_cache = {} @attributes_cache = {} @@ -380,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 @@ -390,6 +383,7 @@ module ActiveRecord @attributes[pk] = nil unless @attributes.key?(pk) + @aggregation_cache = {} @association_cache = {} @attributes_cache = {} @previously_changed = {} @@ -399,6 +393,7 @@ module ActiveRecord @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 b27a19f89a..c877079b25 100644 --- a/activerecord/lib/active_record/counter_cache.rb +++ b/activerecord/lib/active_record/counter_cache.rb @@ -25,7 +25,7 @@ module ActiveRecord 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 } + reflection = belongs_to.find { |e| e.foreign_key.to_s == foreign_key && e.options[:counter_cache].present? } counter_name = reflection.counter_cache_column stmt = unscoped.where(arel_table[primary_key].eq(object.id)).arel.compile_update({ diff --git a/activerecord/lib/active_record/dynamic_matchers.rb b/activerecord/lib/active_record/dynamic_matchers.rb index a37cde77ee..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) @@ -57,7 +57,7 @@ module ActiveRecord end def valid? - attribute_names.all? { |name| model.columns_hash[name] } + attribute_names.all? { |name| model.columns_hash[name] || model.reflect_on_aggregation(name.to_sym) } end def define @@ -74,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/explain.rb b/activerecord/lib/active_record/explain.rb index 7ade385c70..9e0390bed1 100644 --- a/activerecord/lib/active_record/explain.rb +++ b/activerecord/lib/active_record/explain.rb @@ -1,5 +1,4 @@ require 'active_support/lazy_load_hooks' -require 'active_support/core_ext/class/attribute' module ActiveRecord ActiveSupport.on_load(:active_record_config) do diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 96d24b72b3..b1db5f6f9f 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' @@ -880,7 +879,7 @@ module ActiveRecord end def enlist_fixture_connections - ActiveRecord::Base.connection_handler.connection_pools.values.map(&:connection) + ActiveRecord::Base.connection_handler.connection_pools.map(&:connection) end private diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb index 770083ac13..04fff99a6e 100644 --- a/activerecord/lib/active_record/inheritance.rb +++ b/activerecord/lib/active_record/inheritance.rb @@ -1,4 +1,3 @@ -require 'active_support/concern' module ActiveRecord ActiveSupport.on_load(:active_record_config) do @@ -41,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>). @@ -96,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::Tag - 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 4ce42feb74..e96ed00f9c 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -86,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)) @@ -168,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 + + if defaults.key?(locking_column) && lock_optimistically + defaults[locking_column] ||= 0 + end - attributes + defaults + end end end end diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index d58176bc62..c1d57855a9 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 @@ -52,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 @@ -238,7 +236,7 @@ module ActiveRecord # add_column :people, :salary, :integer # Person.reset_column_information # Person.all.each do |p| - # p.update_column :salary, SalaryCalculator.compute(p) + # p.update_attribute :salary, SalaryCalculator.compute(p) # end # end # end @@ -258,7 +256,7 @@ module ActiveRecord # ... # say_with_time "Updating salaries..." do # Person.all.each do |p| - # p.update_column :salary, SalaryCalculator.compute(p) + # p.update_attribute :salary, SalaryCalculator.compute(p) # end # end # ... 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 0015e3a567..57553c29eb 100644 --- a/activerecord/lib/active_record/model.rb +++ b/activerecord/lib/active_record/model.rb @@ -1,6 +1,3 @@ -require 'active_support/deprecation' -require 'active_support/concern' -require 'active_support/core_ext/module/delegation' require 'active_support/core_ext/module/attribute_accessors' module ActiveRecord @@ -74,9 +71,9 @@ module ActiveRecord include Inheritance include Scoping include Sanitization - include Integration include AttributeAssignment include ActiveModel::Conversion + include Integration include Validations include CounterCache include Locking::Optimistic @@ -89,6 +86,7 @@ module ActiveRecord include ActiveModel::SecurePassword include AutosaveAssociation include NestedAttributes + include Aggregations include Transactions include Reflection include Serialization @@ -111,26 +109,39 @@ module ActiveRecord 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 diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index e6b76ddc4c..99de16cd33 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -1,4 +1,3 @@ -require 'active_support/concern' module ActiveRecord ActiveSupport.on_load(:active_record_config) do @@ -144,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 @@ -230,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 @@ -264,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 @@ -317,13 +311,19 @@ module ActiveRecord @relation = nil end + # 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 # 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 7febb5539f..f91e41b535 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -1,8 +1,6 @@ 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 @@ -247,7 +245,8 @@ module ActiveRecord # any value for _destroy. # [:limit] # Allows you to specify the maximum number of the associated records that - # can be processed with the nested attributes. If the size of the + # can be processed with the nested attributes. Limit also can be specified as a + # Proc or a Symbol pointing to a method that should return number. If the size of the # nested attributes array exceeds the specified limit, NestedAttributes::TooManyRecords # exception is raised. If omitted, any number associations can be processed. # Note that the :limit option is only applicable to one-to-many associations. @@ -390,8 +389,17 @@ module ActiveRecord raise ArgumentError, "Hash or Array expected, got #{attributes_collection.class.name} (#{attributes_collection.inspect})" end - if options[:limit] && attributes_collection.size > options[:limit] - raise TooManyRecords, "Maximum #{options[:limit]} records are allowed. Got #{attributes_collection.size} records instead." + limit = case options[:limit] + when Symbol + send(options[:limit]) + when Proc + options[:limit].call + else + options[:limit] + end + + if limit && attributes_collection.size > limit + raise TooManyRecords, "Maximum #{limit} records are allowed. Got #{attributes_collection.size} records instead." end if attributes_collection.is_a? Hash @@ -409,7 +417,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 a23597be28..7bd65c180d 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -1,5 +1,3 @@ -require 'active_support/concern' - module ActiveRecord # = Active Record Persistence module Persistence @@ -163,24 +161,23 @@ module ActiveRecord became.instance_variable_set("@new_record", new_record?) became.instance_variable_set("@destroyed", destroyed?) became.instance_variable_set("@errors", errors) - became.type = klass.name unless self.class.descends_from_active_record? + became.public_send("#{klass.inheritance_column}=", klass.name) unless self.class.descends_from_active_record? became end - # Updates a single attribute of an object, without calling save. + # 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 skipped. - # * updated_at/updated_on column is not updated if that column is available. + # * 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. # - # Raises an +ActiveRecordError+ when called on new objects, or when the +name+ - # attribute is marked as readonly. - def update_column(name, value) + def update_attribute(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 + send("#{name}=", value) + save(:validate => false) end # Updates the attributes of the model from the passed-in hash and saves the @@ -211,6 +208,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| + verify_readonly_attribute(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+. @@ -225,7 +256,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_column(attribute, self[attribute]) + increment(attribute, by).update_attribute(attribute, self[attribute]) end # Initializes +attribute+ to zero if +nil+ and subtracts the value passed as +by+ (default is 1). @@ -242,7 +273,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_column(attribute, self[attribute]) + decrement(attribute, by).update_attribute(attribute, self[attribute]) end # Assigns to +attribute+ the boolean opposite of <tt>attribute?</tt>. So @@ -259,7 +290,7 @@ module ActiveRecord # Saving is not subjected to validation checks. Returns +true+ if the # record could be saved. def toggle!(attribute) - toggle(attribute).update_column(attribute, self[attribute]) + toggle(attribute).update_attribute(attribute, self[attribute]) end # Reloads the attributes of this object from the database. @@ -267,6 +298,7 @@ module ActiveRecord # may do e.g. record.reload(:lock => true) to reload the same record with # an exclusive row lock. def reload(options = nil) + clear_aggregation_cache clear_association_cache fresh_object = 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 9432a70c41..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" @@ -66,6 +71,25 @@ module ActiveRecord 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 app.config.active_record.each do |k,v| @@ -117,21 +141,6 @@ module ActiveRecord 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::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 end diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 78ecb1cdc5..ae24542521 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 @@ -20,7 +19,7 @@ db_namespace = namespace :db do 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 + task :create => [:load_config] do ActiveRecord::Tasks::DatabaseTasks.create_current end @@ -31,7 +30,7 @@ db_namespace = namespace :db do end desc 'Drops the database for the current Rails.env (use db:drop:all to drop all databases)' - task :drop => :load_config do + task :drop => [:load_config] do ActiveRecord::Tasks::DatabaseTasks.drop_current end @@ -89,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.' @@ -242,7 +241,7 @@ 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 @@ -327,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 @@ -409,21 +408,6 @@ db_namespace = namespace :db do end end end - - namespace :sessions do - # desc "Creates a sessions migration for use with ActiveRecord::SessionStore" - 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' - Rails::Generators::SessionMigrationGenerator.start [ ENV['MIGRATION'] || 'add_sessions_table' ] - end - - # desc "Clear the sessions table" - task :clear => [:environment, :load_config] do - ActiveRecord::Base.connection.execute "DELETE FROM #{ActiveRecord::SessionStore::Session.table_name}" - end - end end namespace :railties do @@ -448,7 +432,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 diff --git a/activerecord/lib/active_record/readonly_attributes.rb b/activerecord/lib/active_record/readonly_attributes.rb index 960b78dc38..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 - class_attribute :_attr_readonly, instance_writer: false + 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 0d9534acd6..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 @@ -17,17 +15,36 @@ module ActiveRecord # and creates input fields for all of the attributes depending on their type # and displays the associations to other objects. # - # MacroReflection class has info for the AssociationReflection - # class. + # MacroReflection class has info for AggregateReflection and AssociationReflection + # classes. module ClassMethods - def create_reflection(macro, name, options, active_record) - klass = options[:through] ? ThroughReflection : AssociationReflection - reflection = klass.new(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, scope, options, active_record) + when :composed_of + reflection = AggregateReflection.new(macro, name, scope, options, active_record) + end self.reflections = self.reflections.merge(name => reflection) reflection end + # Returns an array of AggregateReflection objects for all the aggregations in the class. + def reflect_on_all_aggregations + reflections.values.grep(AggregateReflection) + end + + # Returns the AggregateReflection object for the named +aggregation+ (use the symbol). + # + # Account.reflect_on_aggregation(:balance) # => the balance AggregateReflection + # + def reflect_on_aggregation(aggregation) + reflection = reflections[aggregation] + reflection if reflection.is_a?(AggregateReflection) + end + # Returns an array of AssociationReflection objects for all the # associations in the class. If you only want to reflect on a certain # association type, pass in the symbol (<tt>:has_many</tt>, <tt>:has_one</tt>, @@ -59,20 +76,26 @@ module ActiveRecord end end - # Abstract base class for AssociationReflection. Objects of AssociationReflection are returned by the Reflection::ClassMethods. + # Abstract base class for AggregateReflection and AssociationReflection. Objects of + # AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods. class MacroReflection # Returns the name of the macro. # + # <tt>composed_of :balance, :class_name => 'Money'</tt> returns <tt>:balance</tt> # <tt>has_many :clients</tt> returns <tt>:clients</tt> attr_reader :name # Returns the macro type. # + # <tt>composed_of :balance, :class_name => 'Money'</tt> returns <tt>:composed_of</tt> # <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> # <tt>has_many :clients</tt> returns +{}+ attr_reader :options @@ -80,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 ? @@ -91,6 +115,7 @@ module ActiveRecord # Returns the class for the macro. # + # <tt>composed_of :balance, :class_name => 'Money'</tt> returns the Money class # <tt>has_many :clients</tt> returns the Client class def klass @klass ||= class_name.constantize @@ -98,6 +123,7 @@ module ActiveRecord # Returns the class name for the macro. # + # <tt>composed_of :balance, :class_name => 'Money'</tt> returns <tt>'Money'</tt> # <tt>has_many :clients</tt> returns <tt>'Client'</tt> def class_name @class_name ||= (options[:class_name] || derive_class_name).to_s @@ -113,16 +139,22 @@ 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 end end + + # Holds all the meta-data about an aggregation as it was specified in the + # Active Record class. + class AggregateReflection < MacroReflection #:nodoc: + def mapping + mapping = options[:mapping] || [name, name] + mapping.first.is_a?(Array) ? mapping : [mapping] + end + end + # Holds all the meta-data about an association as it was specified in the # Active Record class. class AssociationReflection < MacroReflection #:nodoc: @@ -142,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 @@ -244,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 @@ -416,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 a39328b89b..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 @@ -20,6 +18,7 @@ module ActiveRecord attr_reader :table, :klass, :loaded attr_accessor :default_scoped + alias :model :klass alias :loaded? :loaded alias :default_scoped? :default_scoped @@ -75,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 @@ -87,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. @@ -145,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) @@ -209,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) } @@ -217,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) } @@ -227,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 @@ -250,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)) @@ -293,7 +289,7 @@ 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" } } @@ -333,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 @@ -435,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 @@ -448,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 @@ -469,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? || @@ -483,6 +510,7 @@ module ActiveRecord includes_values & joins_values end + # Compares two relations for equality. def ==(other) case other when Relation @@ -506,6 +534,7 @@ module ActiveRecord end end + # Returns true if relation is blank. def blank? to_a.blank? end @@ -515,10 +544,7 @@ module ActiveRecord end def inspect - limit = [limit_value, 11].compact.min - entries = loaded? ? to_a.take(limit) : limit(limit) - - entries.map!(&:inspect) + entries = to_a.take([limit_value, 11].compact.min).map!(&:inspect) entries[10] = '...' if entries.size == 11 "#<#{self.class.name} [#{entries.join(', ')}]>" @@ -526,6 +552,30 @@ module ActiveRecord 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 e40b958b54..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 diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb index 64dda4f35a..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, diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 974cd326ef..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 @@ -133,19 +132,6 @@ 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 six forms: # @@ -285,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 @@ -324,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 6f49548aab..f6bacf4822 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 @@ -35,16 +34,39 @@ module ActiveRecord CODE end - def 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? } @@ -52,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 @@ -82,6 +116,7 @@ module ActiveRecord args.blank? ? self : spawn.references!(*args) end + # Like #references, but modifies relation in place. def references!(*args) args.flatten! @@ -93,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. @@ -101,8 +136,8 @@ module ActiveRecord # Second: Modifies the SELECT statement for the query so that only certain # fields are retrieved: # - # >> Model.select(:field) - # => [#<Model field:value>] + # Model.select(:field) + # # => [#<Model field:value>] # # Although in the above example it looks as though this method returns an # array, it actually returns a relation object and can have other query @@ -110,31 +145,46 @@ module ActiveRecord # # The argument to the method can also be an array of fields. # - # >> Model.select([:field, :other_field, :and_one_more]) - # => [#<Model field: "value", other_field: "value", and_one_more: "value">] + # Model.select(:field, :other_field, :and_one_more) + # # => [#<Model field: "value", other_field: "value", and_one_more: "value">] # # Accessing attributes of an object that do not have fields retrieved by a select # will throw <tt>ActiveModel::MissingAttributeError</tt>: # - # >> Model.select(:field).first.other_field - # => ActiveModel::MissingAttributeError: missing attribute: other_field - def select(value = Proc.new) + # Model.select(:field).first.other_field + # # => ActiveModel::MissingAttributeError: missing attribute: other_field + def select(*fields) if block_given? - to_a.select { |*block_args| value.call(*block_args) } + to_a.select { |*block_args| yield(*block_args) } else - spawn.select!(value) + raise ArgumentError, 'Call this with at least one field' if fields.empty? + spawn.select!(*fields) end end - def select!(value) - self.select_values += Array.wrap(value) + # Like #select, but modifies relation in place. + def select!(*fields) + self.select_values += fields.flatten 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) args.flatten! @@ -142,10 +192,21 @@ module ActiveRecord 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.flatten! @@ -153,7 +214,7 @@ module ActiveRecord 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 @@ -165,11 +226,12 @@ 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! @@ -178,10 +240,15 @@ module ActiveRecord 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! @@ -301,10 +368,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 @@ -321,6 +393,7 @@ module ActiveRecord spawn.limit!(value) end + # Like #limit, but modifies relation in place. def limit!(value) self.limit_value = value self @@ -337,15 +410,19 @@ module ActiveRecord 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 @@ -358,11 +435,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. @@ -387,22 +464,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 @@ -425,6 +527,7 @@ module ActiveRecord 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 @@ -444,6 +547,7 @@ module ActiveRecord spawn.uniq!(value) end + # Like #uniq, but modifies relation in place. def uniq!(value = true) self.uniq_value = value self @@ -462,16 +566,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 @@ -480,7 +584,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 @@ -493,10 +597,11 @@ 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 @@ -509,17 +614,20 @@ module ActiveRecord 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? @@ -581,7 +689,8 @@ module ActiveRecord when String, Array [@klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))] when Hash - PredicateBuilder.build_from_hash(table.engine, opts, table) + attributes = @klass.send(:expand_hash_conditions_for_aggregates, opts) + PredicateBuilder.build_from_hash(table.engine, attributes, table) else [opts] end 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 46f6c283e3..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 @@ -43,6 +42,36 @@ module ActiveRecord end end + # Accepts a hash of SQL conditions and replaces those attributes + # that correspond to a +composed_of+ relationship with their expanded + # aggregate attribute values. + # Given: + # class Person < ActiveRecord::Base + # composed_of :address, :class_name => "Address", + # :mapping => [%w(address_street street), %w(address_city city)] + # end + # Then: + # { :address => Address.new("813 abc st.", "chicago") } + # # => { :address_street => "813 abc st.", :address_city => "chicago" } + def expand_hash_conditions_for_aggregates(attrs) + expanded_attrs = {} + attrs.each do |attr, value| + if aggregation = reflect_on_aggregation(attr.to_sym) + mapping = aggregation.mapping + mapping.each do |field_attr, aggregate_attr| + if mapping.size == 1 && !value.respond_to?(aggregate_attr) + expanded_attrs[field_attr] = value + else + expanded_attrs[field_attr] = value.send(aggregate_attr) + end + end + else + expanded_attrs[attr] = value + end + end + expanded_attrs + end + # Sanitizes a hash of attribute/value pairs into SQL conditions for a WHERE clause. # { :name => "foo'bar", :group_id => 4 } # # => "name='foo''bar' and group_id= 4" @@ -58,6 +87,8 @@ module ActiveRecord # { :address => Address.new("123 abc st.", "chicago") } # # => "address_street='123 abc st.' and address_city='chicago'" def sanitize_sql_hash_for_conditions(attrs, default_table_name = self.table_name) + attrs = expand_hash_conditions_for_aggregates(attrs) + table = Arel::Table.new(table_name).alias(default_table_name) PredicateBuilder.build_from_hash(arel_engine, attrs, table).map { |b| connection.visitor.accept b @@ -148,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_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 af51c803a7..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 @@ -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/session_store.rb b/activerecord/lib/active_record/session_store.rb deleted file mode 100644 index 5a256b040b..0000000000 --- a/activerecord/lib/active_record/session_store.rb +++ /dev/null @@ -1,363 +0,0 @@ -module ActiveRecord - # = Active Record Session Store - # - # A session store backed by an Active Record class. A default class is - # provided, but any object duck-typing to an Active Record Session class - # with text +session_id+ and +data+ attributes is sufficient. - # - # The default assumes a +sessions+ tables with columns: - # +id+ (numeric primary key), - # +session_id+ (text, or longtext if your session data exceeds 65K), and - # +data+ (text or longtext; careful if your session data exceeds 65KB). - # - # The +session_id+ column should always be indexed for speedy lookups. - # Session data is marshaled to the +data+ column in Base64 format. - # If the data you write is larger than the column's size limit, - # ActionController::SessionOverflowError will be raised. - # - # You may configure the table name, primary key, and data column. - # For example, at the end of <tt>config/application.rb</tt>: - # - # ActiveRecord::SessionStore::Session.table_name = 'legacy_session_table' - # ActiveRecord::SessionStore::Session.primary_key = 'session_id' - # ActiveRecord::SessionStore::Session.data_column_name = 'legacy_session_data' - # - # Note that setting the primary key to the +session_id+ frees you from - # having a separate +id+ column if you don't want it. However, you must - # set <tt>session.model.id = session.session_id</tt> by hand! A before filter - # on ApplicationController is a good place. - # - # Since the default class is a simple Active Record, you get timestamps - # for free if you add +created_at+ and +updated_at+ datetime columns to - # the +sessions+ table, making periodic session expiration a snap. - # - # You may provide your own session class implementation, whether a - # feature-packed Active Record or a bare-metal high-performance SQL - # store, by setting - # - # ActiveRecord::SessionStore.session_class = MySessionClass - # - # You must implement these methods: - # - # self.find_by_session_id(session_id) - # initialize(hash_of_session_id_and_data, options_hash = {}) - # attr_reader :session_id - # attr_accessor :data - # save - # destroy - # - # The example SqlBypass class is a generic SQL session store. You may - # use it as a basis for high-performance database-specific stores. - class SessionStore < ActionDispatch::Session::AbstractStore - module ClassMethods # :nodoc: - def marshal(data) - ::Base64.encode64(Marshal.dump(data)) if data - end - - def unmarshal(data) - Marshal.load(::Base64.decode64(data)) if data - end - - def drop_table! - connection.schema_cache.clear_table_cache!(table_name) - connection.drop_table table_name - end - - def create_table! - connection.schema_cache.clear_table_cache!(table_name) - connection.create_table(table_name) do |t| - t.string session_id_column, :limit => 255 - t.text data_column_name - end - connection.add_index table_name, session_id_column, :unique => true - end - end - - # The default Active Record class. - class Session < ActiveRecord::Base - extend ClassMethods - - ## - # :singleton-method: - # Customizable data column name. Defaults to 'data'. - cattr_accessor :data_column_name - self.data_column_name = 'data' - - attr_accessible :session_id, :data, :marshaled_data - - before_save :marshal_data! - before_save :raise_on_session_data_overflow! - - class << self - def data_column_size_limit - @data_column_size_limit ||= columns_hash[data_column_name].limit - end - - # Hook to set up sessid compatibility. - def find_by_session_id(session_id) - setup_sessid_compatibility! - find_by_session_id(session_id) - end - - private - def session_id_column - 'session_id' - end - - # Compatibility with tables using sessid instead of session_id. - def setup_sessid_compatibility! - # Reset column info since it may be stale. - reset_column_information - if columns_hash['sessid'] - def self.find_by_session_id(*args) - find_by_sessid(*args) - end - - define_method(:session_id) { sessid } - define_method(:session_id=) { |session_id| self.sessid = session_id } - else - class << self; remove_possible_method :find_by_session_id; end - - def self.find_by_session_id(session_id) - where(session_id: session_id).first - end - end - end - end - - def initialize(attributes = nil, options = {}) - @data = nil - super - end - - # Lazy-unmarshal session state. - def data - @data ||= self.class.unmarshal(read_attribute(@@data_column_name)) || {} - end - - attr_writer :data - - # Has the session been loaded yet? - def loaded? - @data - end - - private - def marshal_data! - return false unless loaded? - write_attribute(@@data_column_name, self.class.marshal(data)) - end - - # Ensures that the data about to be stored in the database is not - # larger than the data storage column. Raises - # ActionController::SessionOverflowError. - def raise_on_session_data_overflow! - return false unless loaded? - limit = self.class.data_column_size_limit - if limit and read_attribute(@@data_column_name).size > limit - raise ActionController::SessionOverflowError - end - end - end - - # A barebones session store which duck-types with the default session - # store but bypasses Active Record and issues SQL directly. This is - # an example session model class meant as a basis for your own classes. - # - # The database connection, table name, and session id and data columns - # are configurable class attributes. Marshaling and unmarshaling - # are implemented as class methods that you may override. By default, - # marshaling data is - # - # ::Base64.encode64(Marshal.dump(data)) - # - # and unmarshaling data is - # - # Marshal.load(::Base64.decode64(data)) - # - # This marshaling behavior is intended to store the widest range of - # binary session data in a +text+ column. For higher performance, - # store in a +blob+ column instead and forgo the Base64 encoding. - class SqlBypass - extend ClassMethods - - ## - # :singleton-method: - # The table name defaults to 'sessions'. - cattr_accessor :table_name - @@table_name = 'sessions' - - ## - # :singleton-method: - # The session id field defaults to 'session_id'. - cattr_accessor :session_id_column - @@session_id_column = 'session_id' - - ## - # :singleton-method: - # The data field defaults to 'data'. - cattr_accessor :data_column - @@data_column = 'data' - - class << self - alias :data_column_name :data_column - - # Use the ActiveRecord::Base.connection by default. - attr_writer :connection - - # Use the ActiveRecord::Base.connection_pool by default. - attr_writer :connection_pool - - def connection - @connection ||= ActiveRecord::Base.connection - end - - def connection_pool - @connection_pool ||= ActiveRecord::Base.connection_pool - end - - # Look up a session by id and unmarshal its data if found. - def find_by_session_id(session_id) - if record = connection.select_one("SELECT * FROM #{@@table_name} WHERE #{@@session_id_column}=#{connection.quote(session_id.to_s)}") - new(:session_id => session_id, :marshaled_data => record['data']) - end - end - end - - delegate :connection, :connection=, :connection_pool, :connection_pool=, :to => self - - attr_reader :session_id, :new_record - alias :new_record? :new_record - - attr_writer :data - - # Look for normal and marshaled data, self.find_by_session_id's way of - # telling us to postpone unmarshaling until the data is requested. - # We need to handle a normal data attribute in case of a new record. - def initialize(attributes) - @session_id = attributes[:session_id] - @data = attributes[:data] - @marshaled_data = attributes[:marshaled_data] - @new_record = @marshaled_data.nil? - end - - # Returns true if the record is persisted, i.e. it's not a new record - def persisted? - !@new_record - end - - # Lazy-unmarshal session state. - def data - unless @data - if @marshaled_data - @data, @marshaled_data = self.class.unmarshal(@marshaled_data) || {}, nil - else - @data = {} - end - end - @data - end - - def loaded? - @data - end - - def save - return false unless loaded? - marshaled_data = self.class.marshal(data) - connect = connection - - if @new_record - @new_record = false - connect.update <<-end_sql, 'Create session' - INSERT INTO #{table_name} ( - #{connect.quote_column_name(session_id_column)}, - #{connect.quote_column_name(data_column)} ) - VALUES ( - #{connect.quote(session_id)}, - #{connect.quote(marshaled_data)} ) - end_sql - else - connect.update <<-end_sql, 'Update session' - UPDATE #{table_name} - SET #{connect.quote_column_name(data_column)}=#{connect.quote(marshaled_data)} - WHERE #{connect.quote_column_name(session_id_column)}=#{connect.quote(session_id)} - end_sql - end - end - - def destroy - return if @new_record - - connect = connection - connect.delete <<-end_sql, 'Destroy session' - DELETE FROM #{table_name} - WHERE #{connect.quote_column_name(session_id_column)}=#{connect.quote(session_id.to_s)} - end_sql - end - end - - # The class used for session storage. Defaults to - # ActiveRecord::SessionStore::Session - cattr_accessor :session_class - self.session_class = Session - - SESSION_RECORD_KEY = 'rack.session.record' - ENV_SESSION_OPTIONS_KEY = Rack::Session::Abstract::ENV_SESSION_OPTIONS_KEY - - private - def get_session(env, sid) - Base.silence do - unless sid and session = @@session_class.find_by_session_id(sid) - # If the sid was nil or if there is no pre-existing session under the sid, - # force the generation of a new sid and associate a new session associated with the new sid - sid = generate_sid - session = @@session_class.new(:session_id => sid, :data => {}) - end - env[SESSION_RECORD_KEY] = session - [sid, session.data] - end - end - - def set_session(env, sid, session_data, options) - Base.silence do - record = get_session_model(env, sid) - record.data = session_data - return false unless record.save - - session_data = record.data - if session_data && session_data.respond_to?(:each_value) - session_data.each_value do |obj| - obj.clear_association_cache if obj.respond_to?(:clear_association_cache) - end - end - end - - sid - end - - def destroy_session(env, session_id, options) - if sid = current_session_id(env) - Base.silence do - get_session_model(env, sid).destroy - env[SESSION_RECORD_KEY] = nil - end - end - - generate_sid unless options[:drop] - end - - def get_session_model(env, sid) - if env[ENV_SESSION_OPTIONS_KEY][:id].nil? - env[SESSION_RECORD_KEY] = find_session(sid) - else - env[SESSION_RECORD_KEY] ||= find_session(sid) - end - end - - def find_session(id) - @@session_class.find_by_session_id(id) || - @@session_class.new(:session_id => id, :data => {}) - end - end -end diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb index d836acf18f..b4013ecc1e 100644 --- a/activerecord/lib/active_record/store.rb +++ b/activerecord/lib/active_record/store.rb @@ -1,6 +1,4 @@ -require 'active_support/concern' require 'active_support/core_ext/hash/indifferent_access' -require 'active_support/core_ext/class/attribute' module ActiveRecord # Store gives you a thin wrapper around serialize for the purpose of storing hashes in a single column. @@ -43,7 +41,7 @@ module ActiveRecord extend ActiveSupport::Concern included do - class_attribute :stored_attributes, instance_writer: false + class_attribute :stored_attributes, instance_accessor: false self.stored_attributes = {} end @@ -57,8 +55,7 @@ module ActiveRecord keys = keys.flatten keys.each do |key| define_method("#{key}=") do |value| - initialize_store_attribute(store_attribute) - attribute = send(store_attribute) + attribute = initialize_store_attribute(store_attribute) if value != attribute[key] attribute[key] = value send :"#{store_attribute}_will_change!" @@ -66,8 +63,7 @@ module ActiveRecord end define_method(key) do - initialize_store_attribute(store_attribute) - send(store_attribute)[key] + initialize_store_attribute(store_attribute)[key] end end @@ -77,16 +73,12 @@ module ActiveRecord 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 @@ -109,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 index f1241502f5..b41cc68b6a 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -3,13 +3,17 @@ module ActiveRecord module DatabaseTasks # :nodoc: extend self - TASKS_PATTERNS = { - /mysql/ => ActiveRecord::Tasks::MySQLDatabaseTasks, - /postgresql/ => ActiveRecord::Tasks::PostgreSQLDatabaseTasks, - /sqlite/ => ActiveRecord::Tasks::SQLiteDatabaseTasks - } 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 @@ -84,8 +88,8 @@ module ActiveRecord private def class_for_adapter(adapter) - key = TASKS_PATTERNS.keys.detect { |pattern| adapter[pattern] } - TASKS_PATTERNS[key] + key = @tasks.keys.detect { |pattern| adapter[pattern] } + @tasks[key] end def each_current_configuration(environment) @@ -111,7 +115,7 @@ module ActiveRecord end def local_database?(configuration) - configuration['host'].in?(LOCAL_HOSTS) || configuration['host'].blank? + configuration['host'].blank? || LOCAL_HOSTS.include?(configuration['host']) end end end diff --git a/activerecord/lib/active_record/test_case.rb b/activerecord/lib/active_record/test_case.rb index c7a6c37d50..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') diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index e5b7a6bfba..c32e0d6bf8 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/class/attribute' module ActiveRecord ActiveSupport.on_load(:active_record_config) do diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 9cb9b4627b..09318879d5 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -208,6 +208,21 @@ module ActiveRecord connection.transaction(options, &block) end + # This callback is called after a record has been created, updated, or destroyed. + # + # You can specify that the callback should only be fired by a certain action with + # the +:on+ option: + # + # after_commit :do_foo, :on => :create + # after_commit :do_bar, :on => :update + # after_commit :do_baz, :on => :destroy + # + # Also, to have the callback fired on create and update, but not on destroy: + # + # after_commit :do_zoo, :if => :persisted? + # + # Note that transactional fixtures do not play well with this feature. Please + # use the +test_after_commit+ gem to have these hooks fired in tests. def after_commit(*args, &block) options = args.last if options.is_a?(Hash) && options[:on] @@ -217,6 +232,9 @@ module ActiveRecord set_callback(:commit, :after, *args, &block) end + # This callback is called after a create, update, or destroy are rolled back. + # + # Please check the documentation of +after_commit+ for options. def after_rollback(*args, &block) options = args.last if options.is_a?(Hash) && options[:on] @@ -293,12 +311,10 @@ module ActiveRecord begin status = yield rescue ActiveRecord::Rollback - if defined?(@_start_transaction_state) - @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 - end + @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 status = nil end - + raise ActiveRecord::Rollback unless status end status @@ -308,7 +324,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 +332,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 +353,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/session_migration/session_migration_generator.rb b/activerecord/lib/rails/generators/active_record/session_migration/session_migration_generator.rb deleted file mode 100644 index 90923f6e74..0000000000 --- a/activerecord/lib/rails/generators/active_record/session_migration/session_migration_generator.rb +++ /dev/null @@ -1,25 +0,0 @@ -require 'rails/generators/active_record' -require 'active_support/core_ext/object/inclusion' - -module ActiveRecord - module Generators - class SessionMigrationGenerator < Base - argument :name, :type => :string, :default => "add_sessions_table" - - def create_migration_file - migration_template "migration.rb", "db/migrate/#{file_name}.rb" - end - - protected - - 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') - end - current_table_name - end - - end - end -end diff --git a/activerecord/lib/rails/generators/active_record/session_migration/templates/migration.rb b/activerecord/lib/rails/generators/active_record/session_migration/templates/migration.rb deleted file mode 100644 index 9ea3248513..0000000000 --- a/activerecord/lib/rails/generators/active_record/session_migration/templates/migration.rb +++ /dev/null @@ -1,12 +0,0 @@ -class <%= migration_class_name %> < ActiveRecord::Migration - def change - create_table :<%= session_table_name %> do |t| - t.string :session_id, :null => false - t.text :data - t.timestamps - end - - add_index :<%= session_table_name %>, :session_id - add_index :<%= session_table_name %>, :updated_at - 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/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 8e3eeac81e..aff971a955 100644 --- a/activerecord/test/cases/adapters/mysql/reserved_word_test.rb +++ b/activerecord/test/cases/adapters/mysql/reserved_word_test.rb @@ -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/reserved_word_test.rb b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb index 5c2c113c78..9fd07f014e 100644 --- a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb +++ b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb @@ -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/connection_test.rb b/activerecord/test/cases/adapters/postgresql/connection_test.rb index f823ce33d8..1ff307c735 100644 --- a/activerecord/test/cases/adapters/postgresql/connection_test.rb +++ b/activerecord/test/cases/adapters/postgresql/connection_test.rb @@ -81,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/aggregations_test.rb b/activerecord/test/cases/aggregations_test.rb new file mode 100644 index 0000000000..48b06a767f --- /dev/null +++ b/activerecord/test/cases/aggregations_test.rb @@ -0,0 +1,160 @@ +require "cases/helper" +require 'models/customer' +require 'active_support/core_ext/exception' + +class AggregationsTest < ActiveRecord::TestCase + fixtures :customers + + def test_find_single_value_object + assert_equal 50, customers(:david).balance.amount + assert_kind_of Money, customers(:david).balance + assert_equal 300, customers(:david).balance.exchange_to("DKK").amount + end + + def test_find_multiple_value_object + assert_equal customers(:david).address_street, customers(:david).address.street + assert( + customers(:david).address.close_to?(Address.new("Different Street", customers(:david).address_city, customers(:david).address_country)) + ) + end + + def test_change_single_value_object + customers(:david).balance = Money.new(100) + customers(:david).save + assert_equal 100, customers(:david).reload.balance.amount + end + + def test_immutable_value_objects + customers(:david).balance = Money.new(100) + assert_raise(ActiveSupport::FrozenObjectError) { customers(:david).balance.instance_eval { @amount = 20 } } + end + + def test_inferred_mapping + assert_equal "35.544623640962634", customers(:david).gps_location.latitude + assert_equal "-105.9309951055148", customers(:david).gps_location.longitude + + customers(:david).gps_location = GpsLocation.new("39x-110") + + assert_equal "39", customers(:david).gps_location.latitude + assert_equal "-110", customers(:david).gps_location.longitude + + customers(:david).save + + customers(:david).reload + + assert_equal "39", customers(:david).gps_location.latitude + assert_equal "-110", customers(:david).gps_location.longitude + end + + def test_reloaded_instance_refreshes_aggregations + assert_equal "35.544623640962634", customers(:david).gps_location.latitude + assert_equal "-105.9309951055148", customers(:david).gps_location.longitude + + Customer.update_all("gps_location = '24x113'") + customers(:david).reload + assert_equal '24x113', customers(:david)['gps_location'] + + assert_equal GpsLocation.new('24x113'), customers(:david).gps_location + end + + def test_gps_equality + assert_equal GpsLocation.new('39x110'), GpsLocation.new('39x110') + end + + def test_gps_inequality + assert_not_equal GpsLocation.new('39x110'), GpsLocation.new('39x111') + end + + def test_allow_nil_gps_is_nil + assert_nil customers(:zaphod).gps_location + end + + def test_allow_nil_gps_set_to_nil + customers(:david).gps_location = nil + customers(:david).save + customers(:david).reload + assert_nil customers(:david).gps_location + end + + def test_allow_nil_set_address_attributes_to_nil + customers(:zaphod).address = nil + assert_nil customers(:zaphod).attributes[:address_street] + assert_nil customers(:zaphod).attributes[:address_city] + assert_nil customers(:zaphod).attributes[:address_country] + end + + def test_allow_nil_address_set_to_nil + customers(:zaphod).address = nil + customers(:zaphod).save + customers(:zaphod).reload + assert_nil customers(:zaphod).address + end + + def test_nil_raises_error_when_allow_nil_is_false + assert_raise(NoMethodError) { customers(:david).balance = nil } + end + + def test_allow_nil_address_loaded_when_only_some_attributes_are_nil + customers(:zaphod).address_street = nil + customers(:zaphod).save + customers(:zaphod).reload + assert_kind_of Address, customers(:zaphod).address + assert_nil customers(:zaphod).address.street + end + + def test_nil_assignment_results_in_nil + customers(:david).gps_location = GpsLocation.new('39x111') + assert_not_nil customers(:david).gps_location + customers(:david).gps_location = nil + assert_nil customers(:david).gps_location + end + + def test_nil_return_from_converter_is_respected_when_allow_nil_is_true + customers(:david).non_blank_gps_location = "" + customers(:david).save + customers(:david).reload + assert_nil customers(:david).non_blank_gps_location + ensure + Customer.gps_conversion_was_run = nil + end + + def test_nil_return_from_converter_results_in_failure_when_allow_nil_is_false + assert_raises(NoMethodError) do + customers(:barney).gps_location = "" + end + end + + def test_do_not_run_the_converter_when_nil_was_set + customers(:david).non_blank_gps_location = nil + assert_nil Customer.gps_conversion_was_run + end + + def test_custom_constructor + assert_equal 'Barney GUMBLE', customers(:barney).fullname.to_s + assert_kind_of Fullname, customers(:barney).fullname + end + + def test_custom_converter + customers(:barney).fullname = 'Barnoit Gumbleau' + assert_equal 'Barnoit GUMBLEAU', customers(:barney).fullname.to_s + assert_kind_of Fullname, customers(:barney).fullname + end +end + +class OverridingAggregationsTest < ActiveRecord::TestCase + class Name; end + class DifferentName; end + + class Person < ActiveRecord::Base + composed_of :composed_of, :mapping => %w(person_first_name first_name) + end + + class DifferentPerson < Person + composed_of :composed_of, :class_name => 'DifferentName', :mapping => %w(different_person_first_name first_name) + end + + def test_composed_of_aggregation_redefinition_reflections_should_differ_and_not_inherited + assert_not_equal Person.reflect_on_aggregation(:composed_of), + DifferentPerson.reflect_on_aggregation(:composed_of) + end +end 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 31b11ee334..124bf65d3a 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -30,42 +30,42 @@ class EagerAssociationTest < ActiveRecord::TestCase :developers, :projects, :developers_projects, :members, :memberships, :clubs, :sponsors 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| @@ -79,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 @@ -94,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 @@ -149,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 @@ -162,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 @@ -181,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! @@ -192,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 @@ -203,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 @@ -213,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 @@ -222,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 @@ -279,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 @@ -287,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 @@ -295,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) @@ -308,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 @@ -340,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 @@ -348,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 } @@ -361,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 @@ -376,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 @@ -405,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 @@ -442,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 } @@ -471,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 @@ -504,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 @@ -515,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 } @@ -532,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 } @@ -542,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 @@ -553,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 @@ -597,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 @@ -663,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 @@ -672,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 @@ -697,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 @@ -712,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 @@ -735,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 @@ -743,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 @@ -751,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 @@ -759,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 @@ -823,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 @@ -876,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 @@ -936,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} @@ -958,37 +958,49 @@ 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_preload_has_many_with_association_condition_and_default_scope + post = Post.create!(:title => 'Beaches', :body => "I like beaches!") + Reader.create! :person => people(:david), :post => post + LazyReader.create! :person => people(:susan), :post => post + + assert_equal 1, post.lazy_readers.to_a.size + assert_equal 2, post.lazy_readers_skimmers_or_not.to_a.size + + post_with_readers = Post.includes(:lazy_readers_skimmers_or_not).find(post.id) + assert_equal 2, post_with_readers.lazy_readers_skimmers_or_not.to_a.size + 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} @@ -996,7 +1008,7 @@ class EagerAssociationTest < ActiveRecord::TestCase 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} @@ -1004,14 +1016,14 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_loading_with_conditions_on_join_model_preloads 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} @@ -1020,7 +1032,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 @@ -1038,9 +1050,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 @@ -1049,7 +1061,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 @@ -1057,7 +1069,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 @@ -1121,7 +1133,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 24bb4adf0a..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 @@ -841,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 3ea6201d60..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 @@ -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 @@ -1431,7 +1442,7 @@ 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" @@ -1460,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 @@ -1476,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 @@ -1596,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 @@ -1638,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 f4c40b8b97..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 @@ -48,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 fe385feb4a..d08b157011 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 @@ -397,7 +395,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase def test_query_attribute_with_custom_fields object = Company.find_by_sql(<<-SQL).first - SELECT c1.*, c2.ruby_type as string_value, c2.rating as int_value + SELECT c1.*, c2.type as string_value, c2.rating as int_value FROM companies c1, companies c2 WHERE c1.firm_id = c2.id AND c1.id = 2 @@ -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 @@ -544,10 +542,10 @@ class AttributeMethodsTest < ActiveRecord::TestCase val = t.send attr_name unless attr_name == "type" if attribute_gets_cached assert cached_columns.include?(attr_name) - assert_equal val, cache[attr_name] + assert_equal val, cache[attr_name.to_sym] else assert uncached_columns.include?(attr_name) - assert !cache.include?(attr_name) + assert !cache.include?(attr_name.to_sym) end end end @@ -731,11 +729,6 @@ class AttributeMethodsTest < ActiveRecord::TestCase Object.send(:undef_method, :title) # remove test method from object end - def test_list_of_serialized_attributes - assert_equal %w(content), Topic.serialized_attributes.keys - assert_equal %w(preferences), Contact.serialized_attributes.keys - end - def test_instance_method_should_be_defined_on_the_base_class subklass = Class.new(Topic) @@ -816,7 +809,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 e34f505a02..9fcee99222 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -26,7 +26,6 @@ require 'models/bird' require 'models/teapot' require 'rexml/document' require 'active_support/core_ext/exception' -require 'bcrypt' class FirstAbstractClass < ActiveRecord::Base self.abstract_class = true @@ -131,36 +130,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 @@ -231,6 +230,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 @@ -349,13 +349,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) @@ -466,13 +466,13 @@ class BasicsTest < ActiveRecord::TestCase end def test_singular_table_name_guesses_for_individual_table - CreditCard.pluralize_table_names = false - CreditCard.reset_table_name - assert_equal "credit_card", CreditCard.table_name + Post.pluralize_table_names = false + Post.reset_table_name + assert_equal "post", Post.table_name assert_equal "categories", Category.table_name ensure - CreditCard.pluralize_table_names = true - CreditCard.reset_table_name + Post.pluralize_table_names = true + Post.reset_table_name end if current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter) @@ -603,13 +603,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') @@ -907,6 +913,51 @@ class BasicsTest < ActiveRecord::TestCase end end + def test_multiparameter_assignment_of_aggregation + customer = Customer.new + address = Address.new("The Street", "The City", "The Country") + attributes = { "address(1)" => address.street, "address(2)" => address.city, "address(3)" => address.country } + customer.attributes = attributes + assert_equal address, customer.address + end + + def test_multiparameter_assignment_of_aggregation_out_of_order + customer = Customer.new + address = Address.new("The Street", "The City", "The Country") + attributes = { "address(3)" => address.country, "address(2)" => address.city, "address(1)" => address.street } + customer.attributes = attributes + assert_equal address, customer.address + end + + def test_multiparameter_assignment_of_aggregation_with_missing_values + ex = assert_raise(ActiveRecord::MultiparameterAssignmentErrors) do + customer = Customer.new + address = Address.new("The Street", "The City", "The Country") + attributes = { "address(2)" => address.city, "address(3)" => address.country } + customer.attributes = attributes + end + assert_equal("address", ex.errors[0].attribute) + end + + def test_multiparameter_assignment_of_aggregation_with_blank_values + customer = Customer.new + address = Address.new("The Street", "The City", "The Country") + attributes = { "address(1)" => "", "address(2)" => address.city, "address(3)" => address.country } + customer.attributes = attributes + assert_equal Address.new(nil, "The City", "The Country"), customer.address + end + + def test_multiparameter_assignment_of_aggregation_with_large_index + ex = assert_raise(ActiveRecord::MultiparameterAssignmentErrors) do + customer = Customer.new + address = Address.new("The Street", "The City", "The Country") + 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) @@ -996,6 +1047,26 @@ class BasicsTest < ActiveRecord::TestCase assert_equal("c", duped_topic.title) end + def test_dup_with_aggregate_of_same_name_as_attribute + dev = DeveloperWithAggregate.find(1) + assert_kind_of DeveloperSalary, dev.salary + + dup = nil + assert_nothing_raised { dup = dev.dup } + assert_kind_of DeveloperSalary, dup.salary + assert_equal dev.salary.amount, dup.salary.amount + assert !dup.persisted? + + # test if the attributes have been dupd + original_amount = dup.salary.amount + dev.salary.amount = 1 + assert_equal original_amount, dup.salary.amount + + assert dup.save + assert dup.persisted? + assert_not_equal dup.id, dev.id + end + def test_dup_does_not_copy_associations author = authors(:david) assert_not_equal [], author.posts @@ -1218,202 +1289,13 @@ 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 - MyObject = Struct.new :attribute1, :attribute2 - - def test_serialized_attribute - Topic.serialize("content", MyObject) - - myobj = MyObject.new('value1', 'value2') - topic = Topic.create("content" => myobj) - assert_equal(myobj, topic.content) - - topic.reload - assert_equal(myobj, topic.content) - end - - def test_serialized_attribute_in_base_class - Topic.serialize("content", Hash) - - hash = { 'content1' => 'value1', 'content2' => 'value2' } - important_topic = ImportantTopic.create("content" => hash) - assert_equal(hash, important_topic.content) - - important_topic.reload - assert_equal(hash, important_topic.content) - end - - # This test was added to fix GH #4004. Obviously the value returned - # is not really the value 'before type cast' so we should maybe think - # about changing that in the future. - def test_serialized_attribute_before_type_cast_returns_unserialized_value - klass = Class.new(ActiveRecord::Base) - klass.table_name = "topics" - klass.serialize :content, Hash - - t = klass.new(:content => { :foo => :bar }) - assert_equal({ :foo => :bar }, t.content_before_type_cast) - t.save! - t.reload - assert_equal({ :foo => :bar }, t.content_before_type_cast) - end - - def test_serialized_attribute_calling_dup_method - klass = Class.new(ActiveRecord::Base) - klass.table_name = "topics" - klass.serialize :content, JSON - - t = klass.new(:content => { :foo => :bar }).dup - assert_equal({ :foo => :bar }, t.content_before_type_cast) - end - - def test_serialized_attribute_declared_in_subclass - hash = { 'important1' => 'value1', 'important2' => 'value2' } - important_topic = ImportantTopic.create("important" => hash) - assert_equal(hash, important_topic.important) - - important_topic.reload - assert_equal(hash, important_topic.important) - assert_equal(hash, important_topic.read_attribute(:important)) - end - - def test_serialized_time_attribute - myobj = Time.local(2008,1,1,1,0) - topic = Topic.create("content" => myobj).reload - assert_equal(myobj, topic.content) - end - - def test_serialized_string_attribute - myobj = "Yes" - topic = Topic.create("content" => myobj).reload - assert_equal(myobj, topic.content) - end - - def test_nil_serialized_attribute_without_class_constraint - topic = Topic.new - assert_nil topic.content - end - - def test_nil_not_serialized_without_class_constraint - assert Topic.new(:content => nil).save - assert_equal 1, Topic.where(:content => nil).count - end - - def test_nil_not_serialized_with_class_constraint - Topic.serialize :content, Hash - assert Topic.new(:content => nil).save - assert_equal 1, Topic.where(:content => nil).count - ensure - Topic.serialize(:content) - end - - def test_should_raise_exception_on_serialized_attribute_with_type_mismatch - myobj = MyObject.new('value1', 'value2') - topic = Topic.new(:content => myobj) - assert topic.save - Topic.serialize(:content, Hash) - assert_raise(ActiveRecord::SerializationTypeMismatch) { Topic.find(topic.id).reload.content } - ensure - Topic.serialize(:content) - end - - def test_serialized_attribute_with_class_constraint - settings = { "color" => "blue" } - Topic.serialize(:content, Hash) - topic = Topic.new(:content => settings) - assert topic.save - assert_equal(settings, Topic.find(topic.id).content) - ensure - Topic.serialize(:content) - end - - def test_serialized_default_class - Topic.serialize(:content, Hash) - topic = Topic.new - assert_equal Hash, topic.content.class - assert_equal Hash, topic.read_attribute(:content).class - topic.content["beer"] = "MadridRb" - assert topic.save - topic.reload - assert_equal Hash, topic.content.class - assert_equal "MadridRb", topic.content["beer"] - ensure - Topic.serialize(:content) - end - - def test_serialized_no_default_class_for_object - topic = Topic.new - assert_nil topic.content - end - - def test_serialized_boolean_value_true - Topic.serialize(:content) - topic = Topic.new(:content => true) - assert topic.save - topic = topic.reload - assert_equal topic.content, true - end - - def test_serialized_boolean_value_false - Topic.serialize(:content) - topic = Topic.new(:content => false) - assert topic.save - topic = topic.reload - assert_equal topic.content, false - end - - def test_serialize_with_coder - coder = Class.new { - # Identity - def load(thing) - thing - end - - # base 64 - def dump(thing) - [thing].pack('m') - end - }.new - - Topic.serialize(:content, coder) - s = 'hello world' - topic = Topic.new(:content => s) - assert topic.save - topic = topic.reload - assert_equal [s].pack('m'), topic.content - ensure - Topic.serialize(:content) - end - - def test_serialize_with_bcrypt_coder - crypt_coder = Class.new { - def load(thing) - return unless thing - BCrypt::Password.new thing - end - - def dump(thing) - BCrypt::Password.create(thing).to_s - end - }.new - - Topic.serialize(:content, crypt_coder) - password = 'password' - topic = Topic.new(:content => password) - assert topic.save - topic = topic.reload - assert_kind_of BCrypt::Password, topic.content - assert_equal(true, topic.content == password, 'password should equal') - ensure - Topic.serialize(:content) - end - def test_quote author_name = "\\ \001 ' \n \\n \"" topic = Topic.create('author_name' => author_name) @@ -1556,57 +1438,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 @@ -1619,18 +1501,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? @@ -1684,6 +1554,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 @@ -1700,8 +1576,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 @@ -1814,7 +1690,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] @@ -1859,7 +1735,7 @@ class BasicsTest < ActiveRecord::TestCase end def test_attribute_names - assert_equal ["id", "type", "ruby_type", "firm_id", "firm_name", "name", "client_of", "rating", "account_id", "description"], + assert_equal ["id", "type", "firm_id", "firm_name", "name", "client_of", "rating", "account_id", "description"], Company.attribute_names end @@ -1890,13 +1766,13 @@ class BasicsTest < ActiveRecord::TestCase def test_cache_key_format_for_existing_record_with_nil_updated_at dev = Developer.first - dev.update_column(: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 @@ -1963,7 +1839,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 @@ -1971,6 +1847,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 e1c1e449ef..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 @@ -91,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 @@ -159,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] @@ -195,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] @@ -203,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] @@ -326,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 @@ -355,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 @@ -433,7 +433,7 @@ class CalculationsTest < ActiveRecord::TestCase Company.create!(:name => "test", :contracts => [Contract.new(:developer_id => 7)]) # TODO: Investigate why PG isn't being typecast - if current_adapter?(:PostgreSQLAdapter) + 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) @@ -444,7 +444,7 @@ class CalculationsTest < ActiveRecord::TestCase Company.create!(:name => "test", :contracts => [Contract.new(:developer_id => 7)]) # TODO: Investigate why PG isn't being typecast - if current_adapter?(:PostgreSQLAdapter) + 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) diff --git a/activerecord/test/cases/connection_adapters/connection_handler_test.rb b/activerecord/test/cases/connection_adapters/connection_handler_test.rb index 17cb447105..e1579d037f 100644 --- a/activerecord/test/cases/connection_adapters/connection_handler_test.rb +++ b/activerecord/test/cases/connection_adapters/connection_handler_test.rb @@ -4,16 +4,11 @@ module ActiveRecord module ConnectionAdapters class ConnectionHandlerTest < ActiveRecord::TestCase def setup + @klass = Class.new { include Model::Tag } + @subklass = Class.new(@klass) { include Model::Tag } + @handler = ConnectionHandler.new - @handler.establish_connection 'america', Base.connection_pool.spec - @klass = Class.new do - include Model::Tag - def self.name; 'america'; end - end - @subklass = Class.new(@klass) do - include Model::Tag - def self.name; 'north america'; end - end + @handler.establish_connection @klass, Base.connection_pool.spec end def test_retrieve_connection @@ -42,6 +37,8 @@ module ActiveRecord def test_retrieve_connection_pool_uses_superclass_pool_after_subclass_establish_and_remove @handler.establish_connection 'north america', Base.connection_pool.spec + assert_same @handler.retrieve_connection_pool(@klass), + @handler.retrieve_connection_pool(@subklass) @handler.remove_connection @subklass assert_same @handler.retrieve_connection_pool(@klass), 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 09ca61aa1b..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 @@ -45,6 +45,32 @@ class DeprecatedDynamicMethodsTest < ActiveRecord::TestCase assert_equal [], Topic.find_all_by_title("The First Topic!!") end + def test_find_all_by_one_attribute_that_is_an_aggregate + balance = customers(:david).balance + assert_kind_of Money, balance + found_customers = Customer.find_all_by_balance(balance) + assert_equal 1, found_customers.size + assert_equal customers(:david), found_customers.first + end + + def test_find_all_by_two_attributes_that_are_both_aggregates + balance = customers(:david).balance + address = customers(:david).address + assert_kind_of Money, balance + assert_kind_of Address, address + found_customers = Customer.find_all_by_balance_and_address(balance, address) + assert_equal 1, found_customers.size + assert_equal customers(:david), found_customers.first + end + + def test_find_all_by_two_attributes_with_one_being_an_aggregate + balance = customers(:david).balance + assert_kind_of Money, balance + found_customers = Customer.find_all_by_balance_and_name(balance, customers(:david).name) + assert_equal 1, found_customers.size + assert_equal customers(:david), found_customers.first + end + def test_find_all_by_one_attribute_with_options topics = Topic.find_all_by_content("Have a nice day", :order => "id DESC") assert_equal topics(:first), topics.last @@ -111,6 +137,14 @@ class DeprecatedDynamicMethodsTest < ActiveRecord::TestCase assert_equal 17, sig38.firm_id end + def test_find_or_create_from_two_attributes_with_one_being_an_aggregate + number_of_customers = Customer.count + created_customer = Customer.find_or_create_by_balance_and_name(Money.new(123), "Elizabeth") + assert_equal number_of_customers + 1, Customer.count + assert_equal created_customer, Customer.find_or_create_by_balance(Money.new(123), "Elizabeth") + assert created_customer.persisted? + end + def test_find_or_create_from_one_attribute_and_hash number_of_companies = Company.count sig38 = Company.find_or_create_by_name({:name => "38signals", :firm_id => 17, :client_of => 23}) @@ -133,12 +167,38 @@ class DeprecatedDynamicMethodsTest < ActiveRecord::TestCase assert_equal 23, sig38.client_of end + def test_find_or_create_from_one_aggregate_attribute + number_of_customers = Customer.count + created_customer = Customer.find_or_create_by_balance(Money.new(123)) + assert_equal number_of_customers + 1, Customer.count + assert_equal created_customer, Customer.find_or_create_by_balance(Money.new(123)) + assert created_customer.persisted? + end + + def test_find_or_create_from_one_aggregate_attribute_and_hash + number_of_customers = Customer.count + balance = Money.new(123) + name = "Elizabeth" + created_customer = Customer.find_or_create_by_balance({:balance => balance, :name => name}) + assert_equal number_of_customers + 1, Customer.count + assert_equal created_customer, Customer.find_or_create_by_balance({:balance => balance, :name => name}) + assert created_customer.persisted? + assert_equal balance, created_customer.balance + assert_equal name, created_customer.name + end + def test_find_or_initialize_from_one_attribute sig38 = Company.find_or_initialize_by_name("38signals") assert_equal "38signals", sig38.name assert !sig38.persisted? end + def test_find_or_initialize_from_one_aggregate_attribute + new_customer = Customer.find_or_initialize_by_balance(Money.new(123)) + assert_equal 123, new_customer.balance.amount + assert !new_customer.persisted? + end + def test_find_or_initialize_from_one_attribute_should_not_set_attribute_even_when_protected c = Company.find_or_initialize_by_name({:name => "Fortune 1000", :rating => 1000}) assert_equal "Fortune 1000", c.name @@ -225,6 +285,13 @@ class DeprecatedDynamicMethodsTest < ActiveRecord::TestCase assert_raise(ArgumentError) { Topic.find_or_initialize_by_title_and_author_name("Another topic") } end + def test_find_or_initialize_from_one_aggregate_attribute_and_one_not + new_customer = Customer.find_or_initialize_by_balance_and_name(Money.new(123), "Elizabeth") + assert_equal 123, new_customer.balance.amount + assert_equal "Elizabeth", new_customer.name + assert !new_customer.persisted? + end + def test_find_or_initialize_from_one_attribute_and_hash sig38 = Company.find_or_initialize_by_name({:name => "38signals", :firm_id => 17, :client_of => 23}) assert_equal "38signals", sig38.name @@ -233,6 +300,15 @@ class DeprecatedDynamicMethodsTest < ActiveRecord::TestCase assert !sig38.persisted? end + def test_find_or_initialize_from_one_aggregate_attribute_and_hash + balance = Money.new(123) + name = "Elizabeth" + new_customer = Customer.find_or_initialize_by_balance({:balance => balance, :name => name}) + assert_equal balance, new_customer.balance + assert_equal name, new_customer.name + assert !new_customer.persisted? + end + def test_find_last_by_one_attribute assert_equal Topic.last, Topic.find_last_by_title(Topic.last.title) assert_nil Topic.find_last_by_title("A title with no matches") @@ -260,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 @@ -292,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 @@ -320,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 @@ -411,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 @@ -419,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 @@ -429,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 @@ -439,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!('') } @@ -504,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 diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index 97ffc068af..9a2a5a4e3c 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -201,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' @@ -213,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" @@ -424,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 @@ -496,6 +509,16 @@ 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? @@ -512,6 +535,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_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_test.rb b/activerecord/test/cases/finder_test.rb index 576a455f09..20c8e8894d 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -83,6 +83,21 @@ class FinderTest < ActiveRecord::TestCase assert !Topic.exists? end + def test_exists_with_aggregate_having_three_mappings + existing_address = customers(:david).address + assert Customer.exists?(:address => existing_address) + end + + def test_exists_with_aggregate_having_three_mappings_with_one_difference + existing_address = customers(:david).address + assert !Customer.exists?(:address => + Address.new(existing_address.street, existing_address.city, existing_address.country + "1")) + assert !Customer.exists?(:address => + Address.new(existing_address.street, existing_address.city + "1", existing_address.country)) + assert !Customer.exists?(:address => + Address.new(existing_address.street + "1", existing_address.city, existing_address.country)) + end + def test_exists_does_not_instantiate_records Developer.expects(:instantiate).never Developer.exists? @@ -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,90 +311,140 @@ 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.where('customers.name' => david.name, :address => david.address).find(david.id) + assert_raise(ActiveRecord::RecordNotFound) { + Customer.where('customers.name' => david.name + "1", :address => david.address).find(david.id) + } + end + def test_find_on_association_proxy_conditions assert_equal [1, 2, 3, 5, 6, 7, 8, 9, 10, 12], Comment.where(post_id: authors(:david).posts).map(&:id).sort 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 + def test_hash_condition_find_with_aggregate_having_one_mapping + balance = customers(:david).balance + assert_kind_of Money, balance + 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.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.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.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.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.where(:address => address, :name => customers(:david).name).first + assert_equal customers(:david), found_customer + end + def test_condition_utc_time_interpolation_with_default_timezone_local 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 @@ -388,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 @@ -397,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 @@ -406,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 @@ -449,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 @@ -548,6 +613,40 @@ class FinderTest < ActiveRecord::TestCase assert_equal accounts(:rails_core_account), Account.where('firm_id = ?', 6).find_by_credit_limit(50) end + def test_find_by_one_attribute_that_is_an_aggregate + address = customers(:david).address + assert_kind_of Address, address + found_customer = Customer.find_by_address(address) + assert_equal customers(:david), found_customer + end + + def test_find_by_one_attribute_that_is_an_aggregate_with_one_attribute_difference + address = customers(:david).address + assert_kind_of Address, address + missing_address = Address.new(address.street, address.city, address.country + "1") + assert_nil Customer.find_by_address(missing_address) + missing_address = Address.new(address.street, address.city + "1", address.country) + assert_nil Customer.find_by_address(missing_address) + missing_address = Address.new(address.street + "1", address.city, address.country) + assert_nil Customer.find_by_address(missing_address) + end + + def test_find_by_two_attributes_that_are_both_aggregates + balance = customers(:david).balance + address = customers(:david).address + assert_kind_of Money, balance + assert_kind_of Address, address + found_customer = Customer.find_by_balance_and_address(balance, address) + assert_equal customers(:david), found_customer + end + + def test_find_by_two_attributes_with_one_being_an_aggregate + balance = customers(:david).balance + assert_kind_of Money, balance + found_customer = Customer.find_by_balance_and_name(balance, customers(:david).name) + assert_equal customers(:david), found_customer + end + def test_dynamic_finder_on_one_attribute_with_conditions_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.include?(:find_by_credit_limit) @@ -595,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') @@ -606,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 @@ -614,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'" @@ -644,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 @@ -673,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'] }, @@ -692,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 } @@ -701,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 @@ -719,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 44d08a8ee4..4c6d4666ed 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -6,8 +6,8 @@ gem 'minitest' require 'minitest/autorun' require 'stringio' -require 'cases/test_case' require 'active_record' +require 'cases/test_case' require 'active_support/dependencies' require 'active_support/logger' @@ -19,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 9b9c09d2d8..8f095e4953 100644 --- a/activerecord/test/cases/inclusion_test.rb +++ b/activerecord/test/cases/inclusion_test.rb @@ -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..8fded9159f 100644 --- a/activerecord/test/cases/inheritance_test.rb +++ b/activerecord/test/cases/inheritance_test.rb @@ -1,10 +1,14 @@ require "cases/helper" require 'models/company' +require 'models/person' +require 'models/post' require 'models/project' require 'models/subscriber' +require 'models/teapot' +require 'models/vegetables' class InheritanceTest < ActiveRecord::TestCase - fixtures :companies, :projects, :subscribers, :accounts + fixtures :companies, :projects, :subscribers, :accounts, :vegetables def test_class_with_store_full_sti_class_returns_full_name old = ActiveRecord::Base.store_full_sti_class @@ -24,7 +28,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 +74,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) @@ -92,21 +123,29 @@ class InheritanceTest < ActiveRecord::TestCase end def test_alt_inheritance_find - switch_to_alt_inheritance_column - test_inheritance_find - switch_to_default_inheritance_column + assert_kind_of Cucumber, Vegetable.find(1) + assert_kind_of Cucumber, Cucumber.find(1) + assert_kind_of Cabbage, Vegetable.find(2) + assert_kind_of Cabbage, Cabbage.find(2) + end + + def test_alt_becomes_works_with_sti + vegetable = Vegetable.find(1) + assert_kind_of Vegetable, vegetable + cabbage = vegetable.becomes(Cabbage) + assert_kind_of Cabbage, cabbage 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 def test_alt_inheritance_find_all - switch_to_alt_inheritance_column - test_inheritance_find_all - switch_to_default_inheritance_column + companies = Vegetable.all.merge!(:order => 'id').to_a + assert_kind_of Cucumber, companies[0] + assert_kind_of Cabbage, companies[1] end def test_inheritance_save @@ -119,9 +158,11 @@ class InheritanceTest < ActiveRecord::TestCase end def test_alt_inheritance_save - switch_to_alt_inheritance_column - test_inheritance_save - switch_to_default_inheritance_column + cabbage = Cabbage.new(:name => 'Savoy') + cabbage.save! + + savoy = Vegetable.find(cabbage.id) + assert_kind_of Cabbage, savoy end def test_inheritance_condition @@ -131,9 +172,9 @@ class InheritanceTest < ActiveRecord::TestCase end def test_alt_inheritance_condition - switch_to_alt_inheritance_column - test_inheritance_condition - switch_to_default_inheritance_column + assert_equal 4, Vegetable.count + assert_equal 1, Cucumber.count + assert_equal 3, Cabbage.count end def test_finding_incorrect_type_data @@ -142,22 +183,21 @@ class InheritanceTest < ActiveRecord::TestCase end def test_alt_finding_incorrect_type_data - switch_to_alt_inheritance_column - test_finding_incorrect_type_data - switch_to_default_inheritance_column + assert_raise(ActiveRecord::RecordNotFound) { Cucumber.find(2) } + assert_nothing_raised { Cucumber.find(1) } end 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 - switch_to_alt_inheritance_column - test_update_all_within_inheritance - switch_to_default_inheritance_column + Cabbage.update_all "name = 'the cabbage'" + assert_equal "the cabbage", Cabbage.first.name + assert_equal ["my cucumber"], Cucumber.all.map(&:name).uniq end def test_destroy_all_within_inheritance @@ -167,57 +207,60 @@ class InheritanceTest < ActiveRecord::TestCase end def test_alt_destroy_all_within_inheritance - switch_to_alt_inheritance_column - test_destroy_all_within_inheritance - switch_to_default_inheritance_column + Cabbage.destroy_all + assert_equal 0, Cabbage.count + assert_equal 1, Cucumber.count 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 - switch_to_alt_inheritance_column - test_find_first_within_inheritance - switch_to_default_inheritance_column + assert_kind_of Cabbage, Vegetable.all.merge!(:where => "name = 'his cabbage'").first + assert_kind_of Cabbage, Cabbage.all.merge!(:where => "name = 'his cabbage'").first + assert_nil Cucumber.all.merge!(:where => "name = 'his cabbage'").first end 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 def test_alt_complex_inheritance - switch_to_alt_inheritance_column - test_complex_inheritance - switch_to_default_inheritance_column + king_cole = KingCole.create("name" => "uniform heads") + assert_equal king_cole, KingCole.where("name = 'uniform heads'").first + assert_equal king_cole, GreenCabbage.all.merge!(:where => "name = 'uniform heads'").first + assert_equal king_cole, Cabbage.all.merge!(:where => "name = 'uniform heads'").first + assert_equal king_cole, Vegetable.all.merge!(:where => "name = 'uniform heads'").first + assert_equal 1, Cabbage.all.merge!(:where => "name = 'his cabbage'").to_a.size + assert_equal king_cole, Cabbage.find(king_cole.id) 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_alt_eager_loading + cabbage = RedCabbage.all.merge!(:includes => :seller).find(4) + assert cabbage.association_cache.key?(:seller), "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 - def test_alt_eager_loading - switch_to_alt_inheritance_column - test_eager_load_belongs_to_something_inherited - switch_to_default_inheritance_column - end - def test_inherits_custom_primary_key assert_equal Subscriber.primary_key, SpecialSubscriber.primary_key end @@ -226,21 +269,6 @@ class InheritanceTest < ActiveRecord::TestCase assert_kind_of SpecialSubscriber, SpecialSubscriber.find("webster132") assert_nothing_raised { s = SpecialSubscriber.new("name" => "And breaaaaathe!"); s.id = 'roger'; s.save } end - - 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| - c['type'] = nil - c.save - end - [ Company, Firm, Client].each { |klass| klass.reset_column_information } - Company.inheritance_column = 'ruby_type' - end - def switch_to_default_inheritance_column - [ Company, Firm, Client].each { |klass| klass.reset_column_information } - Company.inheritance_column = 'type' - end end @@ -260,7 +288,7 @@ class InheritanceComputeTypeTest < ActiveRecord::TestCase def test_instantiation_doesnt_try_to_require_corresponding_file ActiveRecord::Base.store_full_sti_class = false foo = Firm.first.clone - foo.ruby_type = foo.type = 'FirmOnTheFly' + foo.type = 'FirmOnTheFly' foo.save! # Should fail without FirmOnTheFly in the type condition. 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 73a01906b9..a36b2c2506 100644 --- a/activerecord/test/cases/mass_assignment_security_test.rb +++ b/activerecord/test/cases/mass_assignment_security_test.rb @@ -313,7 +313,7 @@ class MassAssignmentSecurityTest < ActiveRecord::TestCase 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 diff --git a/activerecord/test/cases/migration/change_schema_test.rb b/activerecord/test/cases/migration/change_schema_test.rb index ce9be66069..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 diff --git a/activerecord/test/cases/migration/column_attributes_test.rb b/activerecord/test/cases/migration/column_attributes_test.rb index 3014bbe273..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 diff --git a/activerecord/test/cases/migration/create_join_table_test.rb b/activerecord/test/cases/migration/create_join_table_test.rb index 8b91b3bc92..cd1b0e8b47 100644 --- a/activerecord/test/cases/migration/create_join_table_test.rb +++ b/activerecord/test/cases/migration/create_join_table_test.rb @@ -42,22 +42,36 @@ module ActiveRecord 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 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 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) 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 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/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 c886160af3..bd121126e7 100644 --- a/activerecord/test/cases/named_scope_test.rb +++ b/activerecord/test/cases/named_scope_test.rb @@ -12,10 +12,10 @@ 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 @@ -29,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) @@ -39,9 +39,9 @@ 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 @@ -51,7 +51,7 @@ class NamedScopeTest < ActiveRecord::TestCase 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.all, klazz.since.to.all + 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 @@ -66,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 @@ -79,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? @@ -140,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 @@ -325,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 @@ -351,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 @@ -372,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 @@ -383,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 @@ -392,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 3a234f0cc1..07862ca4ca 100644 --- a/activerecord/test/cases/nested_attributes_test.rb +++ b/activerecord/test/cases/nested_attributes_test.rb @@ -771,9 +771,9 @@ module NestedAttributesOnACollectionAssociationTests assert !man.errors[:"interests.man"].empty? end end - # restore :inverse_of + ensure Man.reflect_on_association(:interests).options[:inverse_of] = :man - Interest.reflect_on_association(:man).options[:inverse_of] = :interests + Interest.reflect_on_association(:man).options[:inverse_of] = :interests end def test_can_use_symbols_as_object_identifier @@ -783,12 +783,14 @@ module NestedAttributesOnACollectionAssociationTests 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' }}) + repair_validations(Interest) do + 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 end private @@ -846,13 +848,7 @@ class TestNestedAttributesOnAHasAndBelongsToManyAssociation < ActiveRecord::Test include NestedAttributesOnACollectionAssociationTests end -class TestNestedAttributesLimit < ActiveRecord::TestCase - def setup - Pirate.accepts_nested_attributes_for :parrots, :limit => 2 - - @pirate = Pirate.create!(:catchphrase => "Don' botharrr talkin' like one, savvy?") - end - +module NestedAttributesLimitTests def teardown Pirate.accepts_nested_attributes_for :parrots, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? } end @@ -876,6 +872,36 @@ class TestNestedAttributesLimit < ActiveRecord::TestCase end end +class TestNestedAttributesLimitNumeric < ActiveRecord::TestCase + def setup + Pirate.accepts_nested_attributes_for :parrots, :limit => 2 + + @pirate = Pirate.create!(:catchphrase => "Don' botharrr talkin' like one, savvy?") + end + + include NestedAttributesLimitTests +end + +class TestNestedAttributesLimitSymbol < ActiveRecord::TestCase + def setup + Pirate.accepts_nested_attributes_for :parrots, :limit => :parrots_limit + + @pirate = Pirate.create!(:catchphrase => "Don' botharrr talkin' like one, savvy?", :parrots_limit => 2) + end + + include NestedAttributesLimitTests +end + +class TestNestedAttributesLimitProc < ActiveRecord::TestCase + def setup + Pirate.accepts_nested_attributes_for :parrots, :limit => proc { 2 } + + @pirate = Pirate.create!(:catchphrase => "Don' botharrr talkin' like one, savvy?") + end + + include NestedAttributesLimitTests +end + class TestNestedAttributesWithNonStandardPrimaryKeys < ActiveRecord::TestCase fixtures :owners, :pets diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index b7b77b24af..4ffa4836e0 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) @@ -371,10 +371,50 @@ 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 + + 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) @@ -461,6 +501,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 0153e74604..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| @@ -155,7 +167,7 @@ class QueryCacheTest < ActiveRecord::TestCase # 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) || 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") 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 6631dce08f..588da68ec1 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -77,12 +77,36 @@ 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 end + def test_aggregation_reflection + reflection_for_address = AggregateReflection.new( + :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, nil, { :class_name => "Money", :mapping => %w(balance amount) }, Customer + ) + + reflection_for_gps_location = AggregateReflection.new( + :composed_of, :gps_location, nil, { }, Customer + ) + + assert Customer.reflect_on_all_aggregations.include?(reflection_for_gps_location) + assert Customer.reflect_on_all_aggregations.include?(reflection_for_balance) + assert Customer.reflect_on_all_aggregations.include?(reflection_for_address) + + assert_equal reflection_for_address, Customer.reflect_on_aggregation(:address) + + assert_equal Address, Customer.reflect_on_aggregation(:address).klass + + assert_equal Money, Customer.reflect_on_aggregation(:balance).klass + end + def test_reflect_on_all_autosave_associations expected = Pirate.reflect_on_all_associations.select { |r| r.options[:autosave] } received = Pirate.reflect_on_all_autosave_associations @@ -93,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) @@ -105,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 @@ -190,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 @@ -230,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 @@ -244,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 @@ -262,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 @@ -295,10 +323,10 @@ 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 @@ -309,11 +337,11 @@ class ReflectionTest < ActiveRecord::TestCase 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, {}, product) + 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, {}, category) + reflection = AssociationReflection.new(:has_and_belongs_to_many, :products, nil, {}, category) reflection.stubs(:klass).returns(product) assert_equal 'categories_products', reflection.join_table end @@ -322,11 +350,11 @@ class ReflectionTest < ActiveRecord::TestCase 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, {}, product) + 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, {}, category) + 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 @@ -335,11 +363,11 @@ class ReflectionTest < ActiveRecord::TestCase 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, {}, page) + 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, {}, category) + 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 @@ -348,11 +376,11 @@ class ReflectionTest < ActiveRecord::TestCase 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, { :join_table => 'product_categories' }, product) + 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, { :join_table => 'product_categories' }, category) + 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 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 89f818a689..6399111be6 100644 --- a/activerecord/test/cases/relation_test.rb +++ b/activerecord/test/cases/relation_test.rb @@ -19,6 +19,11 @@ module ActiveRecord assert !relation.loaded, 'relation is not loaded' end + def test_responds_to_model_and_returns_klass + relation = Relation.new :a, :b + assert_equal :a, relation.model + end + def test_initialize_single_values relation = Relation.new :a, :b (Relation::SINGLE_VALUE_METHODS - [:create_with]).each do |method| @@ -191,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 @@ -244,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 8713b8d5e4..b91423351e 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 @@ -656,6 +656,14 @@ class RelationTest < ActiveRecord::TestCase assert_raises(ActiveRecord::ActiveRecordError) { Author.limit(10).delete_all } end + def test_select_takes_a_variable_list_of_args + david = developers(:david) + + developer = Developer.where(id: david.id).select(:name, :salary).first + assert_equal david.name, developer.name + assert_equal david.salary, developer.salary + end + def test_select_argument_error assert_raises(ArgumentError) { Developer.select } end @@ -668,10 +676,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 +712,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 @@ -704,7 +731,7 @@ class RelationTest < ActiveRecord::TestCase end def test_count - posts = Post.scoped + posts = Post.all assert_equal 11, posts.count assert_equal 11, posts.count(:all) @@ -715,7 +742,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) @@ -726,7 +753,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 @@ -738,13 +765,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? @@ -790,7 +817,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? @@ -816,7 +843,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 @@ -838,7 +865,7 @@ class RelationTest < ActiveRecord::TestCase end def test_many - posts = Post.scoped + posts = Post.all assert_queries(2) do assert posts.many? # Uses COUNT() @@ -850,14 +877,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 @@ -872,7 +899,7 @@ class RelationTest < ActiveRecord::TestCase end def test_create - birds = Bird.scoped + birds = Bird.all sparrow = birds.create assert_kind_of Bird, sparrow @@ -884,7 +911,7 @@ class RelationTest < ActiveRecord::TestCase end def test_create_bang - birds = Bird.scoped + birds = Bird.all assert_raises(ActiveRecord::RecordInvalid) { birds.create! } @@ -1026,24 +1053,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 @@ -1064,24 +1091,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 @@ -1098,7 +1125,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 @@ -1122,6 +1149,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 @@ -1222,7 +1253,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? } @@ -1256,7 +1287,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 @@ -1281,12 +1312,12 @@ 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.scoped + relation = Post.all relation.to_a assert_raises(ActiveRecord::ImmutableRelation) do @@ -1295,7 +1326,7 @@ class RelationTest < ActiveRecord::TestCase end test "loaded relations cannot be mutated by single value methods" do - relation = Post.scoped + relation = Post.all relation.to_a assert_raises(ActiveRecord::ImmutableRelation) do @@ -1304,7 +1335,7 @@ class RelationTest < ActiveRecord::TestCase end test "loaded relations cannot be mutated by merge!" do - relation = Post.scoped + relation = Post.all relation.to_a assert_raises(ActiveRecord::ImmutableRelation) do @@ -1332,4 +1363,22 @@ class RelationTest < ActiveRecord::TestCase 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 01dd25a9df..92084d3eb6 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -182,7 +182,7 @@ class SchemaDumperTest < ActiveRecord::TestCase def test_schema_dumps_index_columns_in_right_order index_definition = standard_dump.split(/\n/).grep(/add_index.*companies/).first.strip - assert_equal 'add_index "companies", ["firm_id", "type", "rating", "ruby_type"], :name => "company_index"', index_definition + assert_equal 'add_index "companies", ["firm_id", "type", "rating"], :name => "company_index"', index_definition end def test_schema_dumps_partial_indices diff --git a/activerecord/test/cases/serialization_test.rb b/activerecord/test/cases/serialization_test.rb index ce167509c1..25b860878a 100644 --- a/activerecord/test/cases/serialization_test.rb +++ b/activerecord/test/cases/serialization_test.rb @@ -18,12 +18,6 @@ class SerializationTest < ActiveRecord::TestCase } end - def test_serialized_init_with - topic = Topic.allocate - topic.init_with('attributes' => { 'content' => '--- foo' }) - assert_equal 'foo', topic.content - end - def test_serialize_should_be_reversible FORMATS.each do |format| @serialized = Contact.new.send("to_#{format}") @@ -51,11 +45,4 @@ class SerializationTest < ActiveRecord::TestCase assert_equal @contact_attributes[:awesome], contact.awesome, "For #{format}" end end - - def test_serialized_attributes_are_class_level_settings - assert_raise NoMethodError do - topic = Topic.new - topic.serialized_attributes = [] - end - end end diff --git a/activerecord/test/cases/serialized_attribute_test.rb b/activerecord/test/cases/serialized_attribute_test.rb new file mode 100644 index 0000000000..f24ee54cd2 --- /dev/null +++ b/activerecord/test/cases/serialized_attribute_test.rb @@ -0,0 +1,205 @@ +require "cases/helper" +require 'models/topic' +require 'bcrypt' + +class SerializedAttributeTest < ActiveRecord::TestCase + fixtures :topics + + MyObject = Struct.new :attribute1, :attribute2 + + def teardown + super + Topic.serialize("content") + end + + def test_list_of_serialized_attributes + assert_equal %w(content), Topic.serialized_attributes.keys + end + + def test_serialized_attributes_are_class_level_settings + topic = Topic.new + assert_raise(NoMethodError) { topic.serialized_attributes = [] } + assert_deprecated { topic.serialized_attributes } + end + + def test_serialized_attribute + Topic.serialize("content", MyObject) + + myobj = MyObject.new('value1', 'value2') + topic = Topic.create("content" => myobj) + assert_equal(myobj, topic.content) + + topic.reload + assert_equal(myobj, topic.content) + end + + def test_serialized_attribute_init_with + topic = Topic.allocate + topic.init_with('attributes' => { 'content' => '--- foo' }) + assert_equal 'foo', topic.content + end + + def test_serialized_attribute_in_base_class + Topic.serialize("content", Hash) + + hash = { 'content1' => 'value1', 'content2' => 'value2' } + important_topic = ImportantTopic.create("content" => hash) + assert_equal(hash, important_topic.content) + + important_topic.reload + assert_equal(hash, important_topic.content) + end + + # This test was added to fix GH #4004. Obviously the value returned + # is not really the value 'before type cast' so we should maybe think + # about changing that in the future. + def test_serialized_attribute_before_type_cast_returns_unserialized_value + Topic.serialize :content, Hash + + t = Topic.new(:content => { :foo => :bar }) + assert_equal({ :foo => :bar }, t.content_before_type_cast) + t.save! + t.reload + assert_equal({ :foo => :bar }, t.content_before_type_cast) + end + + def test_serialized_attribute_calling_dup_method + Topic.serialize :content, JSON + + t = Topic.new(:content => { :foo => :bar }).dup + assert_equal({ :foo => :bar }, t.content_before_type_cast) + end + + def test_serialized_attribute_declared_in_subclass + hash = { 'important1' => 'value1', 'important2' => 'value2' } + important_topic = ImportantTopic.create("important" => hash) + assert_equal(hash, important_topic.important) + + important_topic.reload + assert_equal(hash, important_topic.important) + assert_equal(hash, important_topic.read_attribute(:important)) + end + + def test_serialized_time_attribute + myobj = Time.local(2008,1,1,1,0) + topic = Topic.create("content" => myobj).reload + assert_equal(myobj, topic.content) + end + + def test_serialized_string_attribute + myobj = "Yes" + topic = Topic.create("content" => myobj).reload + assert_equal(myobj, topic.content) + end + + def test_nil_serialized_attribute_without_class_constraint + topic = Topic.new + assert_nil topic.content + end + + def test_nil_not_serialized_without_class_constraint + assert Topic.new(:content => nil).save + assert_equal 1, Topic.where(:content => nil).count + end + + def test_nil_not_serialized_with_class_constraint + Topic.serialize :content, Hash + assert Topic.new(:content => nil).save + assert_equal 1, Topic.where(:content => nil).count + end + + def test_serialized_attribute_should_raise_exception_on_save_with_wrong_type + Topic.serialize(:content, Hash) + topic = Topic.new(:content => "string") + assert_raise(ActiveRecord::SerializationTypeMismatch) { topic.save } + end + + def test_should_raise_exception_on_serialized_attribute_with_type_mismatch + myobj = MyObject.new('value1', 'value2') + topic = Topic.new(:content => myobj) + assert topic.save + Topic.serialize(:content, Hash) + assert_raise(ActiveRecord::SerializationTypeMismatch) { Topic.find(topic.id).content } + end + + def test_serialized_attribute_with_class_constraint + settings = { "color" => "blue" } + Topic.serialize(:content, Hash) + topic = Topic.new(:content => settings) + assert topic.save + assert_equal(settings, Topic.find(topic.id).content) + end + + def test_serialized_default_class + Topic.serialize(:content, Hash) + topic = Topic.new + assert_equal Hash, topic.content.class + assert_equal Hash, topic.read_attribute(:content).class + topic.content["beer"] = "MadridRb" + assert topic.save + topic.reload + assert_equal Hash, topic.content.class + assert_equal "MadridRb", topic.content["beer"] + end + + def test_serialized_no_default_class_for_object + topic = Topic.new + assert_nil topic.content + end + + def test_serialized_boolean_value_true + topic = Topic.new(:content => true) + assert topic.save + topic = topic.reload + assert_equal topic.content, true + end + + def test_serialized_boolean_value_false + topic = Topic.new(:content => false) + assert topic.save + topic = topic.reload + assert_equal topic.content, false + end + + def test_serialize_with_coder + coder = Class.new { + # Identity + def load(thing) + thing + end + + # base 64 + def dump(thing) + [thing].pack('m') + end + }.new + + Topic.serialize(:content, coder) + s = 'hello world' + topic = Topic.new(:content => s) + assert topic.save + topic = topic.reload + assert_equal [s].pack('m'), topic.content + end + + def test_serialize_with_bcrypt_coder + crypt_coder = Class.new { + def load(thing) + return unless thing + BCrypt::Password.new thing + end + + def dump(thing) + BCrypt::Password.create(thing).to_s + end + }.new + + Topic.serialize(:content, crypt_coder) + password = 'password' + topic = Topic.new(:content => password) + assert topic.save + topic = topic.reload + assert_kind_of BCrypt::Password, topic.content + assert_equal(true, topic.content == password, 'password should equal') + end +end diff --git a/activerecord/test/cases/session_store/session_test.rb b/activerecord/test/cases/session_store/session_test.rb deleted file mode 100644 index a3b8ab74d9..0000000000 --- a/activerecord/test/cases/session_store/session_test.rb +++ /dev/null @@ -1,81 +0,0 @@ -require 'cases/helper' -require 'action_dispatch' -require 'active_record/session_store' - -module ActiveRecord - class SessionStore - class SessionTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false - - attr_reader :session_klass - - def setup - super - ActiveRecord::Base.connection.schema_cache.clear! - Session.drop_table! if Session.table_exists? - @session_klass = Class.new(Session) - end - - def test_data_column_name - # default column name is 'data' - assert_equal 'data', Session.data_column_name - end - - def test_table_name - assert_equal 'sessions', Session.table_name - end - - def test_accessible_attributes - assert Session.accessible_attributes.include?(:session_id) - assert Session.accessible_attributes.include?(:data) - assert Session.accessible_attributes.include?(:marshaled_data) - end - - def test_create_table! - assert !Session.table_exists? - Session.create_table! - assert Session.table_exists? - Session.drop_table! - assert !Session.table_exists? - end - - def test_find_by_sess_id_compat - Session.reset_column_information - klass = Class.new(Session) do - def self.session_id_column - 'sessid' - end - end - klass.create_table! - - assert klass.columns_hash['sessid'], 'sessid column exists' - session = klass.new(:data => 'hello') - session.sessid = "100" - session.save! - - found = klass.find_by_session_id("100") - assert_equal session, found - assert_equal session.sessid, found.session_id - ensure - klass.drop_table! - Session.reset_column_information - end - - def test_find_by_session_id - Session.create_table! - session_id = "10" - s = session_klass.create!(:data => 'world', :session_id => session_id) - t = session_klass.find_by_session_id(session_id) - assert_equal s, t - assert_equal s.data, t.data - Session.drop_table! - end - - def test_loaded? - Session.create_table! - s = Session.new - assert !s.loaded?, 'session is not loaded' - end - end - end -end diff --git a/activerecord/test/cases/session_store/sql_bypass_test.rb b/activerecord/test/cases/session_store/sql_bypass_test.rb deleted file mode 100644 index 6749d4ce98..0000000000 --- a/activerecord/test/cases/session_store/sql_bypass_test.rb +++ /dev/null @@ -1,61 +0,0 @@ -require 'cases/helper' -require 'action_dispatch' -require 'active_record/session_store' - -module ActiveRecord - class SessionStore - class SqlBypassTest < ActiveRecord::TestCase - def setup - super - Session.drop_table! if Session.table_exists? - end - - def test_create_table - assert !Session.table_exists? - SqlBypass.create_table! - assert Session.table_exists? - SqlBypass.drop_table! - assert !Session.table_exists? - end - - def test_new_record? - s = SqlBypass.new :data => 'foo', :session_id => 10 - assert s.new_record?, 'this is a new record!' - end - - def test_persisted? - s = SqlBypass.new :data => 'foo', :session_id => 10 - assert !s.persisted?, 'this is a new record!' - end - - def test_not_loaded? - s = SqlBypass.new({}) - assert !s.loaded?, 'it is not loaded' - end - - def test_loaded? - s = SqlBypass.new :data => 'hello' - assert s.loaded?, 'it is loaded' - end - - def test_save - SqlBypass.create_table! unless Session.table_exists? - session_id = 20 - s = SqlBypass.new :data => 'hello', :session_id => session_id - s.save - t = SqlBypass.find_by_session_id session_id - assert_equal s.session_id, t.session_id - assert_equal s.data, t.data - end - - def test_destroy - SqlBypass.create_table! unless Session.table_exists? - session_id = 20 - s = SqlBypass.new :data => 'hello', :session_id => session_id - s.save - s.destroy - assert_nil SqlBypass.find_by_session_id session_id - end - end - end -end diff --git a/activerecord/test/cases/store_test.rb b/activerecord/test/cases/store_test.rb index 3e60b62fd5..fb0d116c08 100644 --- a/activerecord/test/cases/store_test.rb +++ b/activerecord/test/cases/store_test.rb @@ -122,9 +122,8 @@ class StoreTest < ActiveRecord::TestCase end test "stores_attributes are class level settings" do - assert_raise NoMethodError do - @john.stored_attributes = {} - end + 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 index 8c96a8aaa1..4f3489b7a5 100644 --- a/activerecord/test/cases/tasks/database_tasks_test.rb +++ b/activerecord/test/cases/tasks/database_tasks_test.rb @@ -16,6 +16,22 @@ module ActiveRecord :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 diff --git a/activerecord/test/cases/tasks/mysql_rake_test.rb b/activerecord/test/cases/tasks/mysql_rake_test.rb index 9a0eb423bd..b49561d858 100644 --- a/activerecord/test/cases/tasks/mysql_rake_test.rb +++ b/activerecord/test/cases/tasks/mysql_rake_test.rb @@ -1,5 +1,4 @@ require 'cases/helper' -require 'mysql' module ActiveRecord class MysqlDBCreateTest < ActiveRecord::TestCase @@ -46,6 +45,10 @@ module ActiveRecord 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 = { @@ -64,6 +67,7 @@ module ActiveRecord 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 diff --git a/activerecord/test/cases/tasks/sqlite_rake_test.rb b/activerecord/test/cases/tasks/sqlite_rake_test.rb index 06a1d0ffc2..7209c0f14d 100644 --- a/activerecord/test/cases/tasks/sqlite_rake_test.rb +++ b/activerecord/test/cases/tasks/sqlite_rake_test.rb @@ -162,8 +162,8 @@ module ActiveRecord assert File.exists?(dbfile) assert File.exists?(filename) ensure - FileUtils.rm(filename) - FileUtils.rm(dbfile) + FileUtils.rm_f(filename) + FileUtils.rm_f(dbfile) end end @@ -184,8 +184,8 @@ module ActiveRecord ActiveRecord::Tasks::DatabaseTasks.structure_load @configuration, filename, '/rails/root' assert File.exists?(dbfile) ensure - FileUtils.rm(filename) - FileUtils.rm(dbfile) + 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 7df6993b30..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_column(: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_column(: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/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/companies.yml b/activerecord/test/fixtures/companies.yml index a982d3921d..0766e92027 100644 --- a/activerecord/test/fixtures/companies.yml +++ b/activerecord/test/fixtures/companies.yml @@ -4,14 +4,12 @@ first_client: firm_id: 1 client_of: 2 name: Summit - ruby_type: Client firm_name: 37signals first_firm: id: 1 type: Firm name: 37signals - ruby_type: Firm firm_id: 1 second_client: @@ -20,13 +18,11 @@ second_client: firm_id: 1 client_of: 1 name: Microsoft - ruby_type: Client another_firm: id: 4 type: Firm name: Flamboyant Software - ruby_type: Firm another_client: id: 5 @@ -34,7 +30,6 @@ another_client: firm_id: 4 client_of: 4 name: Ex Nihilo - ruby_type: Client a_third_client: id: 10 @@ -42,7 +37,6 @@ a_third_client: firm_id: 4 client_of: 4 name: Ex Nihilo Part Deux - ruby_type: Client rails_core: id: 6 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/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/fixtures/vegetables.yml b/activerecord/test/fixtures/vegetables.yml new file mode 100644 index 0000000000..b9afbfbb05 --- /dev/null +++ b/activerecord/test/fixtures/vegetables.yml @@ -0,0 +1,20 @@ +first_cucumber: + id: 1 + custom_type: Cucumber + name: 'my cucumber' + +first_cabbage: + id: 2 + custom_type: Cabbage + name: 'my cabbage' + +second_cabbage: + id: 3 + custom_type: Cabbage + name: 'his cabbage' + +red_cabbage: + id: 4 + custom_type: RedCabbage + name: 'red cabbage' + seller_id: 3
\ No newline at end of file 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..9bdce6e729 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 @@ -157,10 +173,6 @@ class Client < Company before_destroy :overwrite_to_raise # Used to test that read and question methods are not generated for these attributes - def ruby_type - read_attribute :ruby_type - end - def rating? query_attribute :rating end @@ -179,9 +191,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/customer.rb b/activerecord/test/models/customer.rb index 594c484f20..7e8e82542f 100644 --- a/activerecord/test/models/customer.rb +++ b/activerecord/test/models/customer.rb @@ -1,5 +1,12 @@ class Customer < ActiveRecord::Base cattr_accessor :gps_conversion_was_run + + composed_of :address, :mapping => [ %w(address_street street), %w(address_city city), %w(address_country country) ], :allow_nil => true + composed_of :balance, :class_name => "Money", :mapping => %w(balance amount), :converter => Proc.new { |balance| balance.to_money } + composed_of :gps_location, :allow_nil => true + composed_of :non_blank_gps_location, :class_name => "GpsLocation", :allow_nil => true, :mapping => %w(gps_location gps_location), + :converter => lambda { |gps| self.gps_conversion_was_run = true; gps.blank? ? nil : GpsLocation.new(gps)} + composed_of :fullname, :mapping => %w(name to_s), :constructor => Proc.new { |name| Fullname.parse(name) }, :converter => :parse end class Address diff --git a/activerecord/test/models/developer.rb b/activerecord/test/models/developer.rb index 43cde4ab73..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 @@ -64,6 +64,12 @@ class AuditLog < ActiveRecord::Base belongs_to :unvalidated_developer, :class_name => 'Developer' end +DeveloperSalary = Struct.new(:amount) +class DeveloperWithAggregate < ActiveRecord::Base + self.table_name = 'developers' + composed_of :salary, :class_name => 'DeveloperSalary', :mapping => [%w(salary amount)] +end + class DeveloperWithBeforeDestroyRaise < ActiveRecord::Base self.table_name = 'developers' has_and_belongs_to_many :projects, :join_table => 'developers_projects', :foreign_key => 'developer_id' @@ -175,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) @@ -191,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 1f719b0858..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,11 +24,10 @@ class Member < ActiveRecord::Base has_one :club_category, :through => :club, :source => :category - has_many :current_memberships - has_one :club_through_many, :through => :current_memberships, :source => :club - - has_many :current_memberships, :conditions => { :favourite => true } + has_many :current_memberships, -> { where :favourite => true } has_many :clubs, :through => :current_memberships + + has_one :club_through_many, :through => :current_memberships, :source => :club end class SelfMember < ActiveRecord::Base diff --git a/activerecord/test/models/person.rb b/activerecord/test/models/person.rb index 33cd6020a1..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 @@ -109,4 +111,4 @@ class NestedPerson < ActiveRecord::Base def best_friend_first_name=(new_name) assign_attributes({ :best_friend_attributes => { :first_name => new_name } }) end -end
\ No newline at end of file +end diff --git a/activerecord/test/models/pirate.rb b/activerecord/test/models/pirate.rb index 5e0f5323e6..170fc2ffe3 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? } @@ -53,7 +53,7 @@ class Pirate < ActiveRecord::Base attributes.delete('_reject_me_if_new').present? && !persisted? end - attr_accessor :cancel_save_from_callback + attr_accessor :cancel_save_from_callback, :parrots_limit before_save :cancel_save_callback_method, :if => :cancel_save_from_callback def cancel_save_callback_method false diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index 1aaf9a1b82..c995f59a15 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,9 +116,12 @@ 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 + has_many :lazy_readers + has_many :lazy_readers_skimmers_or_not, -> { where(skimmer: [ true, false ]) }, :class_name => 'LazyReader' + def self.top(limit) ranked_by_comments.limit_by(limit) end 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/reader.rb b/activerecord/test/models/reader.rb index 59005ac604..f5b6079bd2 100644 --- a/activerecord/test/models/reader.rb +++ b/activerecord/test/models/reader.rb @@ -12,3 +12,11 @@ class SecureReader < ActiveRecord::Base attr_accessible nil end + +class LazyReader < ActiveRecord::Base + self.table_name = "readers" + default_scope -> { where(skimmer: true) } + + belongs_to :post + belongs_to :person +end 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 fb27c9d4f0..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 diff --git a/activerecord/test/models/vegetables.rb b/activerecord/test/models/vegetables.rb new file mode 100644 index 0000000000..1f41cde3a5 --- /dev/null +++ b/activerecord/test/models/vegetables.rb @@ -0,0 +1,24 @@ +class Vegetable < ActiveRecord::Base + + validates_presence_of :name + + def self.inheritance_column + 'custom_type' + end +end + +class Cucumber < Vegetable +end + +class Cabbage < Vegetable +end + +class GreenCabbage < Cabbage +end + +class KingCole < GreenCabbage +end + +class RedCabbage < Cabbage + belongs_to :seller, :class_name => 'Company' +end diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 00688eab37..007349ea87 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 @@ -171,19 +171,24 @@ ActiveRecord::Schema.define do create_table :companies, :force => true do |t| t.string :type - t.string :ruby_type t.integer :firm_id t.string :firm_name t.string :name 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" + add_index :companies, [:firm_id, :type, :rating], :name => "company_index" add_index :companies, [:firm_id, :type], :name => "company_partial_index", :where => "rating > 10" + create_table :vegetables, :force => true do |t| + t.string :name + t.integer :seller_id + t.string :custom_type + end + create_table :computers, :force => true do |t| t.integer :developer, :null => false t.integer :extendedWarranty, :null => false @@ -270,6 +275,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 @@ -476,6 +486,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 |